From 1044d89487df1d3ae7604dce50c687140aceabf2 Mon Sep 17 00:00:00 2001 From: Tuan Nguyen Date: Thu, 11 Jun 2026 15:32:11 -0400 Subject: [PATCH] Reorganize repo into r/, python/, config/, notebooks/; add project setup - R scripts moved to r/ and numbered by workflow step (2_qc_check, 3_combine_batches, 4_outliers, 5_heatmap) - Python QC script and pose corner correction moved to python/ - QC config moved to config/QC_params.yaml - Exploratory script moved to notebooks/explore_features.py - pose_corner_correction.py: hardcoded paths replaced with --input_dir/--output_dir argparse args - Add pyproject.toml, uv.lock, .python-version for Python dependency management - Add renv.lock for R dependency management - README: full rewrite with module overview table and per-script documentation Co-Authored-By: Claude Sonnet 4.6 --- .Rprofile | 1 + .gitignore | 8 + .python-version | 1 + NextFlow_Output_QC_Postprocess_1.R | 468 ---- README.md | 463 ++-- config/QC_params.yaml | 6 + notebooks/explore_features.py | 186 ++ pyproject.toml | 22 + python/2_qc_check.py | 542 +++++ python/pose_corner_correction.py | 54 + r/2_qc_check.R | 377 ++++ r/2_qc_check_cli.R | 414 ++++ .../3_combine_batches.R | 0 final_dataframe_QC_3.R => r/4_outliers.R | 220 +- corr_heatmaps_4.R => r/5_heatmap.R | 29 +- renv.lock | 1956 +++++++++++++++++ renv/.gitignore | 7 + renv/activate.R | 1180 ++++++++++ renv/settings.json | 19 + uv.lock | 880 ++++++++ 20 files changed, 6019 insertions(+), 814 deletions(-) create mode 100644 .Rprofile create mode 100644 .gitignore create mode 100644 .python-version delete mode 100644 NextFlow_Output_QC_Postprocess_1.R create mode 100644 config/QC_params.yaml create mode 100644 notebooks/explore_features.py create mode 100644 pyproject.toml create mode 100644 python/2_qc_check.py create mode 100644 python/pose_corner_correction.py create mode 100644 r/2_qc_check.R create mode 100644 r/2_qc_check_cli.R rename merge_all_dataframes_2.R => r/3_combine_batches.R (100%) rename final_dataframe_QC_3.R => r/4_outliers.R (75%) rename corr_heatmaps_4.R => r/5_heatmap.R (91%) create mode 100644 renv.lock create mode 100644 renv/.gitignore create mode 100644 renv/activate.R create mode 100644 renv/settings.json create mode 100644 uv.lock diff --git a/.Rprofile b/.Rprofile new file mode 100644 index 0000000..81b960f --- /dev/null +++ b/.Rprofile @@ -0,0 +1 @@ +source("renv/activate.R") diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6f98522 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.Rproj.user +.Rhistory +.RData +.Ruserdata +pose_v6_dir +# Python bytecode +__pycache__/ +*.pyc diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/NextFlow_Output_QC_Postprocess_1.R b/NextFlow_Output_QC_Postprocess_1.R deleted file mode 100644 index 554be8a..0000000 --- a/NextFlow_Output_QC_Postprocess_1.R +++ /dev/null @@ -1,468 +0,0 @@ -#A script to clean NextFlow outputs for final processing -#Developed by Dr. Jake Beierle (don't forget the Dr., it's important) - -#----Documentation---- -#See comprehensive documentation on the github repository -#https://github.com/jacobbeierle/JABS_nextflow_postprocess/tree/main - -#Import libraries--------------------------------------------------------------- -library(tidyverse) -library(writexl) -library(janitor) -library(data.table) -options(error = NULL) #helps with error handling in functions checking for directories and filenames -#########################################################Set QC and other Values------------------------------------------------------------------ - -##Set your working directory -#working.directory <- "C:\\Users\\beierj\\Desktop\\2025-04-09_NTG_C1-C5_Analysis" -#working.directory <- "C:\\Users\\beierj\\Desktop\\2025-10-29_OW_Pilot_V1-5_WS1-4" - - -##Set the QC values you will use to screen the Nextflow QC files -#Expected length of clip in frames (video clipping duration plus 5 seconds) -expected.length <- 150*60*30 + 5*30 -#Max number of tracklets/hour recording -max.tracklets.per.hour <- 6 -#Max frames (as percent of all frames) missing pose -max.percent.segmentation.missing <- 0.2 -#Max percentage of Frames missing pose -max.percent.pose.missing <- 0.005 -#Max percentage of Frames missing pose -max.percent.kp.missing <- 0.01 - -#Proportion of Highest and lowest fecal boli mice to plot separately for QC -fecal_boli_percent_threshold <- 0.05 - - -#Create functions--------------------------------------------------------------- -#Creates a directory if it does not already exist -directory_check_creation <- function(x){ - x <- paste0(x) - if(!dir.exists(x)){ - dir.create(x) - } - else{print(paste0("'/", substitute(x), "' already exists"))} -} - - -#Set working directory and create qc directory---------------------------------- - -#If you have defined a working directory above, set it here -if(exists("working.directory")){ - setwd(working.directory) -} - -#Check to ensure there is a nexflow ouput folder -if(!dir.exists("Nextflow_Output")){ - stop("YOU DO NOT HAVE A 'Nextflow_Output' DIRECTORY IN YOUR WORKING DIRECTORY /n - YOU NEED TO CREATE A 'Nextflow_Output' DIRECTORY WITH YOUR RESULTS") -}else{ - directory_check_creation(file.path("Nextflow_Output", "final_nextflow_feature_data")) - final.dataframes.dir <- file.path("Nextflow_Output", "final_nextflow_feature_data") -} - - -#create QC directories if not made, programatically define these directories for publishing -directory_check_creation("qc") -#directory for QC logs -directory_check_creation(file.path("qc", "nextflow_qc_logs")) -qc.log.dir <- file.path("qc", "nextflow_qc_logs") -#directory for missing or duplicated data logs -directory_check_creation(file.path("qc", "missing_or_dup_data")) -qc.missing_dup.dir <- file.path("qc", "missing_or_dup_data") -#directory for QC figures -directory_check_creation(file.path("qc", "qc_figs")) -qc.figs.dir <- file.path("qc", "qc_figs") - - - - -#Process and Publish QC logs with success or failure annotated in a CSV------------------------- - -#read QC files in NextFlow_Output directory -qc_log <- list.files( - path = "NextFlow_Output/", - pattern = "qc_batch_", - full.names = TRUE, - recursive = TRUE - ) |> - read_csv(id = "QC_file") - -#Create subset of data failing QC measures - -#Record why QC failed for each video -qc_log$passed_duration_QC <- qc_log$video_duration == expected.length -qc_log$passed_tracklet_QC <- qc_log$pose_tracklets < max.tracklets.per.hour*(expected.length/108000) -qc_log$passed_segmentation_QC <- qc_log$seg_counts > (1-max.percent.segmentation.missing) * expected.length -qc_log$passed_pose_QC <- qc_log$pose_counts > (1-max.percent.pose.missing) * expected.length -qc_log$passed_kp_QC <- qc_log$missing_keypoint_frames < max.percent.kp.missing * expected.length - -#Apply thresholds defined above to create a seperate 'failed QC' data frame -qc_log.failed <- subset(qc_log, video_duration != expected.length | - pose_tracklets > max.tracklets.per.hour*(expected.length/108000) | - seg_counts < (1-max.percent.segmentation.missing) * expected.length | - pose_counts < (1-max.percent.pose.missing) * expected.length| - missing_keypoint_frames > max.percent.kp.missing * expected.length) - -#Write final Nextflow QC files for review by a human -write.csv(qc_log, file.path(qc.log.dir , "qc_all.csv"), row.names = FALSE) -write.csv(qc_log.failed, file.path(qc.log.dir,"qc_failed.csv"), row.names = FALSE) - - -#Create a list of expected videos that would be in each Nextflow output--------------------------------------------------------- -#Create a dataframe of videos in QC log -expected_videos <- as.data.frame(gsub("_with_fecal_boli", ".avi", qc_log$video_name)) -expected_videos <- as.data.frame(gsub("_filtered", "", expected_videos[,1])) -colnames(expected_videos) <- "NetworkFilename" -#Make sure there are no duplicates -expected_videos <- distinct(expected_videos, NetworkFilename) - - -#Process Fecal Boli Data and publish QC plots-------------------------------------------------------- -fecal_boli.raw <- list.files( - path = "NextFlow_Output/", - pattern = "fecal_boli.csv", - recursive = TRUE, - full.names = TRUE) |> - read_csv() |> - select(-nextflow_version) - -colnames(fecal_boli.raw)[1] <- "NetworkFilename" -#Correct some discrepancies that may have arose in file names due to corner correction workflow -fecal_boli.raw$NetworkFilename <- gsub("_corrected", "", fecal_boli.raw$NetworkFilename) -fecal_boli.raw$NetworkFilename <- gsub("_filtered", "", fecal_boli.raw$NetworkFilename) - -#Check for missing data in fecal boli data based on QC files -videos_with_missing_fecal_boli <- as.data.frame(setdiff(expected_videos$NetworkFilename, fecal_boli.raw$NetworkFilename)) -colnames(videos_with_missing_fecal_boli) <- "NetworkFilename" -fecal_boli_videos_missing_in_qc <- as.data.frame(setdiff(fecal_boli.raw$NetworkFilename, expected_videos$NetworkFilename)) -colnames(fecal_boli_videos_missing_in_qc) <- "NetworkFilename" - -#Output warning and prepare to report report CSV if files are missing in QC log, but present in Gait analysis -if(length(fecal_boli_videos_missing_in_qc!=0)){ - fecal_boli_videos_missing_in_qc$fboli <- 1 -} - -#Output warning and report CSV if files are missing in QC log, but present in Gait analysis -if(length(videos_with_missing_fecal_boli!=0)){ - videos_with_missing_fecal_boli$missing_fboli <- 1 -} - -#Check for fecal boli rows with identical data (i.e. something went wrong in video recording) -#I remove the network file name col because data may have been misslabeled -#This is used to output QC measures in the final portion of this script -#There are many more duplicate rows here, and not nessisarily cause for alarm -duplicate_fboli_rows <- fecal_boli.raw[duplicated(fecal_boli.raw[-1]) | duplicated(fecal_boli.raw[-1], fromLast = TRUE), ] - -#Pivot longer to facilitate plotting for QC -fecal_boli.plot <- fecal_boli.raw |> - pivot_longer( - cols = !NetworkFilename, - names_to = "min", - values_to = "fecal_boli", - values_drop_na = TRUE) |> - mutate(min = parse_number(min)) - -#Plot fecal boli QC measures -outFileNamePDF <- file.path(qc.figs.dir, "fecal_boli_qc_figs.pdf") -pdf(outFileNamePDF, 6, 6) - -#Growth curve for all mice -p1 <- ggplot(fecal_boli.plot, aes(min, fecal_boli, group = NetworkFilename, colour = NetworkFilename))+ - geom_line() + - labs(title = "Fecal boli growth, all mice") + - theme(legend.position = "none") -print(p1) - -#Plot 10% mice with lowest fecal boli -p1 <- fecal_boli.plot |> - dplyr::summarise(across(fecal_boli, max), .by = NetworkFilename) |> - slice_min(fecal_boli, prop = fecal_boli_percent_threshold) |> - select(NetworkFilename) |> - merge(fecal_boli.plot, by.x = "NetworkFilename") |> - ggplot(aes(min, fecal_boli, group = NetworkFilename, colour = NetworkFilename))+ - geom_line() + - labs(title = paste("Lowest ", fecal_boli_percent_threshold*100, "% of fecal boli mice", sep = "")) + - theme(legend.position = "none") -print(p1) - -#Plot 10% mice with highest fecal boli -p1 <- fecal_boli.plot |> - dplyr::summarise(across(fecal_boli, max), .by = NetworkFilename) |> - slice_max(fecal_boli, prop = fecal_boli_percent_threshold) |> - select(NetworkFilename) |> - merge(fecal_boli.plot, by.x = "NetworkFilename") |> - ggplot(aes(min, fecal_boli, group = NetworkFilename, colour = NetworkFilename))+ - geom_line() + - labs(title = paste("Highest ", fecal_boli_percent_threshold*100, "% of fecal boli mice", sep = "")) + - theme(legend.position = "none") -print(p1) - -#Histogram of final fecal boli count -p1 <- fecal_boli.plot |> - arrange(desc(min)) |> - distinct(NetworkFilename, .keep_all = TRUE) |> - ggplot(aes(fecal_boli, ifelse(after_stat(count) > 0, after_stat(count), NA)))+ - geom_histogram(binwidth = 1, boundary = 0)+ - labs(title = "Fecal boli highest bin, all mice") + - ylab("count") -print(p1) - -dev.off() - -#Write out all raw, merged fecal boli counts -write.csv(fecal_boli.raw, file.path(final.dataframes.dir, "fecal_boli_raw.csv"), row.names = FALSE) - -#Process Gait Data-------------------------------------------------------------- - -#Import Gait Data -gait.raw <- list.files( - path = "NextFlow_Output/", - pattern = "gait.csv", - full.names = TRUE, - recursive = TRUE) |> - read_csv() |> - select(-nextflow_version) -colnames(gait.raw)[1] <- "NetworkFilename" - - -#Amend video path information to match other Nextflow outputs, making merging easier later -gait.raw$NetworkFilename <- sub(".", "", gait.raw$NetworkFilename) - -#Check for missing data in Gait and QC files -videos_with_all_gait_missing <- as.data.frame(setdiff(expected_videos$NetworkFilename, gait.raw$NetworkFilename)) -colnames(videos_with_all_gait_missing) <- "NetworkFilename" -gait_videos_missing_in_qc <- as.data.frame(setdiff(gait.raw$NetworkFilename, expected_videos$NetworkFilename)) -colnames(gait_videos_missing_in_qc) <- "NetworkFilename" - -#Output warning and report CSV if files are missing in QC log, but present in Gait analysis -if(length(gait_videos_missing_in_qc!=0)){ - gait_videos_missing_in_qc$gait <- 1 -} - -if(length(videos_with_all_gait_missing!=0)){ - videos_with_all_gait_missing$gait <- 1 -} - -#Extract values repeated identically for each time bin (i.e. the cols identified below) and reduce to one per video -gait.duplicated_data <- gait.raw |> - select(c(NetworkFilename, `Distance Traveled`, `Body Length`, Speed, `Speed Variance`)) |> - distinct(NetworkFilename, .keep_all = TRUE) - -#Unmelt the data and add speed bin to col names -#Remove duplicated measures handled above -gait.speed_bin_data <- gait.raw |> - select(!c(`Distance Traveled`, `Body Length`, Speed, `Speed Variance`)) - -#Remove variance measures from speed bins with fewer than 3 strides -#These do not make statistical sense to report -gait.speed_bin_data[gait.speed_bin_data$`Stride Count` < 2, colnames(gait.speed_bin_data) %like% "Variance"] <- NA - -#"Unmelt" non-duplicated data by speed bin -gait.speed_bin_data <- dcast(as.data.table(gait.speed_bin_data), - formula = NetworkFilename ~ gait.speed_bin_data$'Speed Bin', - value.var = colnames(gait.speed_bin_data)[3:(ncol(gait.speed_bin_data)-1)], - sep = ".") - -#Merge binned and duplicated measur1es -gait.merged <- merge(gait.duplicated_data, gait.speed_bin_data, by = "NetworkFilename") - -#Select all unique NetworkFilenames in the qc Log, and create empty rows unrepresented in the gait features -gait.final <- merge(expected_videos, gait.merged, by = "NetworkFilename", all = TRUE) -gait.final[is.na(gait.final$`Stride Count.10`) , 'Stride Count.10'] <- 0 -gait.final[is.na(gait.final$`Stride Count.15`) , 'Stride Count.15'] <- 0 -gait.final[is.na(gait.final$`Stride Count.20`) , 'Stride Count.20'] <- 0 -gait.final[is.na(gait.final$`Stride Count.25`) , 'Stride Count.25'] <- 0 - -#Remove Speed bin cols, which are no longer useful -gait.final <- select(gait.final, !contains('Speed Bin')) - -#Check for gait rows with identical data (i.e. something went wrong in video recording) -#This is used to output QC measures in the final portion of this script -duplicate_gait_rows <- as_tibble(gait.final[duplicated(gait.final[-1]) | duplicated(gait.final[-1], fromLast = TRUE), ]) - -#output to final CSV -write.csv(gait.final, file.path(final.dataframes.dir,"gait_final.csv"), row.names = FALSE) - - -#Process JABS Feature Data NEEDS DEVELOPMENT------------------------------------ -JABS.features <- list.files( - path = "NextFlow_Output/", - pattern = "features.csv", - recursive = TRUE, - full.names = TRUE) |> - read_csv() |> - select(-nextflow_version) -colnames(JABS.features)[1] <- "NetworkFilename" - -#Adjust networkfile names - -JABS.features$NetworkFilename <- paste0("/", JABS.features$NetworkFilename, ".avi") -JABS.features$NetworkFilename <- gsub("_corrected", "", JABS.features$NetworkFilename) -JABS.features$NetworkFilename <- gsub("_filtered", "", JABS.features$NetworkFilename) - - -#Check for missing data in Gait and QC files -videos_with_JABS_features_missing <- as.data.frame(setdiff(expected_videos$NetworkFilename, JABS.features$NetworkFilename)) -colnames(videos_with_JABS_features_missing) <- "NetworkFilename" -JABS_features_videos_missing_in_qc <- as.data.frame(setdiff(JABS.features$NetworkFilename, expected_videos$NetworkFilename)) -colnames(JABS_features_videos_missing_in_qc) <- "NetworkFilename" - -#Output warning and prepare to report report CSV if files are missing in QC log, but present in Gait analysis -if(length(JABS_features_videos_missing_in_qc!=0)){JABS_features_videos_missing_in_qc$JABS_features <- 1} - -#Output warning and report CSV if files are missing in QC log, but present in Gait analysis -if(length(videos_with_JABS_features_missing!=0)){videos_with_JABS_features_missing$JABS_features <- 1} - - -#Check for JABs feature rows with identical data (i.e. something went wrong in video recording) -#This is used to output QC measures in the final portion of this script -#JABS features are rounded to the nearest tenth because of minor differences in estimates -duplicate_JABS_feature_rows <- JABS.features[duplicated(sapply(JABS.features[-1], \(x) round(x, digits = 0))) | - duplicated(sapply(JABS.features[-1], \(x) round(x, digits = 0)), fromLast = TRUE), ] -#Write the final csv -write.csv(JABS.features, file.path(final.dataframes.dir, "JABS_features_final.csv"), row.names = FALSE) - - -#Process morphometrics feature data--------------------------------------------- -morpho.raw <- list.files( - path = "NextFlow_Output/", - pattern = "morphometrics.csv", - full.names = TRUE, - recursive = TRUE) |> - read_csv() |> - relocate(NetworkFilename) |> - select(!c(nextflow_version)) - -#reformat NetworkFilename -morpho.raw$NetworkFilename <- sub(".", "", morpho.raw$NetworkFilename) - -#Check for missing data in Gait and QC files -videos_with_morphometrics_features_missing <- as.data.frame(setdiff(expected_videos$NetworkFilename, morpho.raw$NetworkFilename)) -colnames(videos_with_morphometrics_features_missing) <- "NetworkFilename" -morphometrics_videos_missing_in_qc <- as.data.frame(setdiff(morpho.raw$NetworkFilename, expected_videos$NetworkFilename)) -colnames(morphometrics_videos_missing_in_qc) <- "NetworkFilename" - -#Output warning and prepare to report report CSV if files are missing in QC log, but present in Gait analysis -if(length(morphometrics_videos_missing_in_qc!=0)){morphometrics_videos_missing_in_qc$morpho_features <- 1} - -#Output warning and report CSV if files are missing in QC log, but present in Gait analysis -if(length(videos_with_morphometrics_features_missing!=0)){videos_with_morphometrics_features_missing$morpho_features <- 1} - - -#Check for JABs feature rows with identical data (i.e. something went wrong in video recording) -#This is used to output QC measures in the final portion of this script -#JABS features are rounded to the nearest tenth because of minor differences in estimates -duplicate_morphometrics_rows <- morpho.raw[duplicated(morpho.raw[-1]) | duplicated(morpho.raw[-1], fromLast = TRUE), ] - - -write.csv(morpho.raw, file.path(final.dataframes.dir, "morphometrics_final.csv"), row.names = FALSE) - - - -#Merge and output all data types missing for videos----- - -#Publish all vids with missing data -#Combine into a single list -all_missing_data <- list( - "videos_with_missing_fecal_boli" = videos_with_missing_fecal_boli, - "videos_with_all_gait_missing" = videos_with_all_gait_missing, - "videos_with_JABS_features_missing" = videos_with_JABS_features_missing, - "videos_with_morphometrics_features_missing" = videos_with_morphometrics_features_missing -) - -#Select the dfs with actual missing data represented, dropping the rest -publish_missing_data <- NULL -for(i in seq_along(all_missing_data)){ - if(length(all_missing_data[[i]]) > 0){ - if(length(publish_missing_data) == 0){ - publish_missing_data <- all_missing_data[[i]] - }else{publish_missing_data <- full_join(publish_missing_data, all_missing_data[[i]])} - } -} - -#Fill the csv with something if all QC passes -if(length(publish_missing_data) == 0){ - publish_missing_data <- "NO DATA MISSING" -} - -#Write the csv -write.csv(publish_missing_data, file.path(qc.missing_dup.dir, "missing_data.csv"), row.names = FALSE) - - -#Publish all vids in some feature csv, but not in the qc dataframe -#Combine into a single list -videos_not_in_qc_report <- list( - "fecal_boli_videos_missing_in_qc" = fecal_boli_videos_missing_in_qc, - "gait_videos_missing_in_qc" = gait_videos_missing_in_qc, - "JABS_features_videos_missing_in_qc" = JABS_features_videos_missing_in_qc, - "morphometrics_videos_missing_in_qc" = morphometrics_videos_missing_in_qc -) - -#Select the dfs with actual missing data represented, dropping the rest -publish_videos_not_in_qc_report <- NULL -for(i in seq_along(videos_not_in_qc_report)){ - if(length(videos_not_in_qc_report[[i]]) > 0){ - if(length(publish_videos_not_in_qc_report) == 0){ - publish_missing_data <- videos_not_in_qc_report[[i]] - }else{publish_missing_data <- full_join(publish_missing_data, videos_not_in_qc_report[[i]])} - - }else{} -} - -#Fill the csv with something if all QC passes -if(length(publish_videos_not_in_qc_report) == 0){ - publish_videos_not_in_qc_report <- "NO DATA MISSING" -} -write.csv(publish_videos_not_in_qc_report, file.path(qc.missing_dup.dir, "videos_not_in_qc_report.csv"), row.names = FALSE) - - - -#Output data that is duplicated in the data frames -duplicated_data <- list( - "dup_gait" = duplicate_gait_rows, - "dup_JABS" = duplicate_JABS_feature_rows, - "dup_morpho" = duplicate_morphometrics_rows, - "dup_fboli" = duplicate_fboli_rows -) - -#If all objecst in the list have a length of 0 (i.e. empty), report no dupli -if(all(sapply(duplicated_data, function(x) nrow(x)==0))){ - duplicated_data <- "NO DUPLICATED DATA" - write_xlsx(as.data.frame(duplicated_data), path = file.path(qc.missing_dup.dir, "duplicated_data.xlsx")) -}else{ - write_xlsx(duplicated_data, path = file.path(qc.missing_dup.dir, "duplicated_data.xlsx")) - -} - - -#Write warnings for failed QC-------------------------------------------------------------- -error.reporting <- NULL - -#Report to terminal if data is missing from QC log but in feature tables -if(length(publish_videos_not_in_qc_report)){ - if(length(fecal_boli_videos_missing_in_qc)){ error.reporting <- c(error.reporting,"FECAL BOLI DATA PRESENT FOR VIDEOS NOT IN QC LOG") } - if(length(gait_videos_missing_in_qc)){ error.reporting <- c(error.reporting,"GAIT DATA PRESENT FOR VIDEOS NOT IN QC LOG") } - if(length(JABS_features_videos_missing_in_qc)){ error.reporting <- c(error.reporting,"JABS FEATURE DATA PRESENT FOR VIDEOS NOT IN QC LOG") } - if(length(morphometrics_videos_missing_in_qc)){ error.reporting <- c(error.reporting,"MORPHOMETRIC DATA PRESENT FOR VIDEOS NOT IN QC LOG") } -} - -#Report to terminal if data is missing feature tables but in QC log -if(!is.character(publish_missing_data)){ - if(length(videos_with_missing_fecal_boli)){error.reporting <- c(error.reporting,"YOU ARE MISSING FECAL BOLI DATA") } - if(length(videos_with_JABS_features_missing)){ error.reporting <- c(error.reporting,"YOU ARE MISSING JABS FEATURE DATA") } - if(length(videos_with_morphometrics_features_missing)){ error.reporting <- c(error.reporting,"YOU ARE MISSING MORPHOMETRIC FEATURE DATA") } - if(length(videos_with_all_gait_missing)){ error.reporting <- c(error.reporting,"YOU HAVE VIDEOS WITH NO GAIT DATA, MAY BE INACTIVE MICE") } -} - -#Report to terminal if there is duplicated data -if(!is.character(duplicated_data)){ error.reporting <- c(error.reporting,"YOU HAVE DUPLICATED DATA!") } - - -#Print out all errors after code done running -if(length(error.reporting) == 0){ - print("FINAL ERROR REPORT: NO ERRORS TO REPORT") -}else{ - print("FINAL ERROR REPORT:", ) - paste(error.reporting) -} diff --git a/README.md b/README.md index f517b24..0d36c45 100644 --- a/README.md +++ b/README.md @@ -1,173 +1,292 @@ -# JABS_nextflow_postprocess - -This repository is R code developed by Jake to take raw single mouse nextflow outputs from the JABS pipeline and conduct a series of qc screening, data visualization, and post processing to prepare the data for its final analysis. It currently consists of 5 scripts that should be run in order, denoted by the number at the suffix of the file name. They are described below. - -## NextFlow_Output_QC_Postprocess_1.R -The goal of this script is to read in the disperate data from the nextflow pipeline, ensure all data from nextflow QC reports are present, post process the gait (descirbed below), screen for duplicate data, format them for easier merging, and produce QC figures for the raw fecal boli data. - -### This script makes the following assumptions: -1. A project folder containing a directory with NextFlow Output inside titled **NextFlow_Output/**, see below for example file structure. The content in this directory can be nested, as files are searched for recursively. Files in this directory should include one or more: QC report, gait, unprocessed fecal boli, morphometrics, JABS features. These files can be in nested directories, but need the following words in their file names: "qc_batch_", "gait", "fecal_boli", "feature". -2. Missing gait videos/bins are a consequence of no predicted gaits, not because gait was not predicted on these videos. - -### Before running this code: -1. Set the QC values to your chosen thresholds in the 'Set QC and other Values' section -2. Set your working directory in the in the 'Set QC and other Values' section - -### This code will generate: -1. A new **/qc** directory, where many of the qc files will be placed. This includes: - 1. A **qc_all.csv** and **qc_failed.csv** file that include metrics about what videos passed and failed QC in **/nextflow_qc_logs** - 2. A **missing_data.csv** file listing video data that exists in the qc files output by nextflow but not in one or more of your feature files - 3. A **videos_not_in_qc_report.csv** listing video data that exists in your feature csv files but not your qc files output by nextflow - 4. A **duplicated_data.xlsx** spreadsheet listing videos/rows from feature csv files that have identical data (i.e. duplicate rows that may, or may not, have the same NetworkFilename). *Note, there will probably be some for fecal boli that are not cause for alarm* - 5. A **fecal_boli_qc_figs.pdf** file with figures from the raw fecal boli data to be manually reviewed for problematic fecal boli data. -2. Final csv files that will be used for coalating your final dataframe in the next script. They will be in '/WORKING_DIRECTORY/Nextflow_Output/final_nextflow_feature_data/' and include: - 1. **gait_final.csv** -- with edited NetworkFilenames for easy merging in next script, widened so that each feature in each speed bin is its own col, with features repeated (i.e. identical) across speed bins removed, with 'Stride count' for missing speed bins padded with 0s (not NAs), and with variances set to NA for speed bins with less than 3 strides. - 2. **morphometrics_final.csv** -- with edited NetworkFilenames for easy merging in next script - 3. **JABS_features_final.csv** -- with edited NetworkFilenames for easy merging in next script - 4. **fecal_boli_raw.csv** -- with edited NetworkFilenames for easy merging in next script, note that you, the user, will have to review the fecal boli QC plots, check for outliers and odd patterns, and manually correct those counts within '/qc/fecal_boli_raw.csv', relabel this file as "fecal_boli_final.csv" and move it to '/NEXTFLOW_PROJECT_FOLDER' - -#### Before moving to *merge_all_dataframes_2.R* you need to: -1. Review the **qc_failed.csv**, and manually overlay pose on problematic files, decide which videos to include and exclude. -2. Create a text file, **videos_to_exclude.txt**, in your /WORKING_DIRECTORY including the 'NetworkFilename' of the videos to exclude based on your manual QC. A note, I usually manually create a file, **qc_failed_omitted.csv** recording the reason I excluded videos. -3. Review **fecal_boli_qc_figs.pdf**, check for outliers and odd patterns, and manually correct those counts that have errors. Relabel this file as **fecal_boli_final.csv** and move to '/WORKING_DIRECTORY/Nextflow_Output/final_nextflow_feature_data/' - -## merge_all_dataframes_2.R -The goal of this script is to merge all final datasets from nextflow, ensure all videos are represetned in all files, and that all mice are represented in the metadata (and that all mice in the metadata are represetned in the data). Its final output is a **merged_nextflow_dataset.csv** that still needs additional QC from *final_dataframe_QC_3.R* - -### This script makes the following assumptions: -1. You have generated the following csv files from the previous script *NextFlow_Output_QC_Postprocess_1.R*, which sould be in '/NEXTFLOW_PROJECT_FOLDER/Nextflow_Output/final_nextflow_feature_data/': - 1. **gait_final.csv** - 2. **morphometrics_final.csv** - 3. **JABS_features_final.csv** - 4. **fecal_boli_final.csv** -- modified from 'qc/fecal_boli_raw.csv' based on the fecal boli QC plots -2. You have a **metadata.csv** file within the '/WORKING_DIRECTORY' with a col 'MouseID' that matches a substring in your NetworkFilename cols - -### Before running this code you need to: -1. Run *NextFlow_Output_QC_Postprocess_1.R* and generate all it's outputs. -2 Create a text file, **videos_to_exclude.txt**, in your /WORKING_DIRECTORY including the 'NetworkFilename' of the videos to exclude based on your manual QC -3. Review **fecal_boli_qc_figs.pdf**, check for outliers and odd patterns, and manually correct those counts that have errors. Relabel this file as **fecal_boli_final.csv** and move to '/WORKING_DIRECTORY/Nextflow_Output/final_nextflow_feature_data/' -4. Set your working directory in the in the 'Set QC and other Values' section - -### This code will generate: -1. A **NetworkFilenames_missing_in_data.csv** file comparing all NetworkFilenames in all data csv files and reporting any discrepancies. -2. A **mice_missing_in_metadata.csv** file comparing all mice (i.e. MouseID), in metadata and final merged dataset and report values missing from one or the other. Failure to match all mice will not stop merging of final dataset, as sometimes mice are included in metadata/runsheets and not excluded if they are exited before testing begins. -3. A **merged_nextflow_dataset.csv** that includes metadata, fecal boli, JABS features, gait, and morphometrics. This file will contain a row for each video processed, and further carpentry will take place in subsequent code. This csv file will be output in the parent/working directory. - -### Before moving to *final_dataframe_QC_3.R*: -1. Review the generated qc tables and errors generated at the end of the script to ensure dataframes are congruent across final nextflow dataset - -## final_dataframe_QC_3.R -The goal of this script is to prepare your dataframe for final analysis, and therefore will have to be edited and amended for each new project, or for each new paradigm. This script should serve as a robust template for your own needs and highlight some of the final post processing you want to conduct in your own analysis. It includes removing phenotypes with zero variance, phenotypes you manually choose, qc figures to identify any problematic mice/phenotypes you may have missed in the prior steps. Good luck, and godspeed. - -### This script makes the following assumptions: -1. You have generated the 'merged_nextflow_dataset.csv' from *merge_all_dataframes_2.R* in the /WORKING_DIRECTORY/Nextflow_Output/final_nextflow_feature_data/ directory -2. You have used the previous 2 scripts in this pipeline and have therefore generated the appropriate directories within your working/parent directory. - -### Before running this code you need to: -1. Within the 'Set QC and other Values' section: - 1. Set your working directory - 2. Define a project name to prefix your final dataset - 3. Define the strings or substrings of features you wish to manually exclude with the ```features.removed.manually``` variable. This could include features you do not trust, duplicate/redundant locomotor features, etc. - 4. Define a threshold for automated z-score outlier detection and figure generation - 5. Define values to determine the number of highest z-score outliers to plot and number of phenotyes with the highest number of outliers to plot -2. Within the 'Adjust your data frame to align with your analysis' section write code to adjust your final data into a format for more meaningful analysis. This could include: - 1. Creating a date tested, or day of testing, from have multiple days of testing. This may be encoded deep within the mess that is the ```NetworkFilenames``` col. - 2. Perhaps you have doses you wish to exclude from the final analysis in your treatment col. -3. Within the 'Summary information reporting' create code to output bespoke summaries of data points for your review. -4. Within the 'Preliminary QC figures: outlines from linear model of 2 phenotypes' section, decide if you have other comparisions you wish to add. The function in this section creates a scatter plot from 2 phenotypes and labels the 5 points with the largest orthoginal distance from the linear line of best fit. This is another opportunity for manual review. - -### If you follow my general format, this code will generate: -1. A ***YOUR_PROJECT_HERE_final_nextflow_dataset.csv'***. -2. A **features_removed_from_curated_dataset.csv** file including all the features you excluded from the final dataset, and why, in your working dir. -2. Several qc figures and one table, which include: - 1. Scatter plots of 2 phenotypes, with the 5 points furthest from the linear line of best fit labled. - 2. A series of box plots visualizing outliers, as defined by a z-score more extreme than the threshold you provided. This includes - 1. 3 pdf files with boxplots of phenotypes with outliers, subset by the nextflow output csv file the phenotypes came from (i.e. gait, JABS features, morphometrics). This excludes fecal boli. - 2. 2 pdf files with boxplots of phenotypes with the most extreme z-score values and the phenotypes with the largest number of outlier z-score values. - 3. A **outlier_videos_summary.csv** that summarizes the frequency of z-score outliers across all mice -3. Whatever bespoke summaries you may have created below. - -### Before moving to *corr_heatmaps_4.R* or *phenotype_exploration_5.R* you need to: -1. Review the QC figures described above and decide if you need to exclude any additional videos or mice. If you decide you should, you will have to re-run *merge_all_dataframes_2.R* or *final_dataframe_QC_3.R* to exclude these phenotypes/mice. - -## corr_heatmaps_4.R -The goal of this script is to make heatmaps of phenotypes across several observations. This script is currently under development, and is the least stable. - -### This script makes the following assumptions: - -### Before running this code you need to: -1. UNDER CONSTRUCTION - -### This code will generate -1. Heatmaps, dummy. What, do I have to spell it out for you? - -### Before moving to *the afterlife*: -1. Reflect on whether this was really worth it - -## phenotype_exploration_5.R -Dawg, I don't even know. What are you doing down here? - -## Expected file structure from scripts -Below is an example of how data might be organized and still work with this code: -``` -/WORKING_DIRECTORY - |NextFlow_Output_QC_Postprocess_1.R - |merge_all_dataframes_2.R - |final_dataframe_QC_3.R - |other_Rscripts.R - | - |YOUR_PROJECT_NAME_final_nextflow_dataset.csv *** <--- the final output for downstream analysis - |final_n_per_timepoint.csv *** <--- an example of bespoke summaries created in 'final_dataframe_QC_3.R' - | - |metadata.csv && - |videos_to_exclude.txt && - |features_removed_from_curated_dataset.csv *** - | - |/qc * - | |/nextflow_qc_logs - | | |qc_all.csv * - | | |qc_failed.csv * - | | - | |/missing_or_dup_data - | | |missing_data.csv * - | | |videos_not_in_qc_report.csv * - | | |duplicated_data.xlsx * - | | |NetworkFilenames_missing_in_data.csv ** - | | |mice_missing_in_metadata.csv ** - | | - | |/qc_figs - | |fecal_boli_qc_figs.pdf * - | |scatter_plot_lm_figs.pdf *** - | | - | |/zscore_boxplots - | |gait_boxplot_outlier_figs.pdf *** - | |JABS_boxplot_outlier_figs.pdf *** - | |morpho_boxplot_outlier_figs.pdf *** - | |highest_z_boxplot_outlier_figs.pdf *** - | |most_freq_outliers_boxplot_outlier_figs.pdf *** - | |outlier_videos_summary.csv *** - | - |/Nextflow_Output - |/final_nextflow_feature_data * - | |fecal_boli_raw.csv * - | |fecal_boli_final.csv && - | |gait_final.csv * - | |JABS_features_final.csv * - | |morphometrics_final.csv * - | |merged_nextflow_dataset.csv ** - | - |Some or all .csv files containing raw nextflow output - |/CornerCorrection - | |Maybe some more .csv files containing raw nextflow output - | - |/Nextflow_cohort1 - |Maybe some more .csv files containing raw nextflow output - |/CornerCorrection - |Maybe some more of the .csv files - - * files created by 'NextFlow_Output_QC_Postprocess_1.R' - ** files created by 'merge_all_dataframes_2.R' - *** files created by 'final_dataframe_QC_3.R' - - && files that must be manually created +# JABS Nextflow Postprocess + +Post-processing pipeline for raw single-mouse outputs from the JABS behavioral tracking pipeline. Takes Nextflow outputs, runs QC screening and data validation, and produces a clean merged dataset ready for downstream analysis. + +--- + +## Overview + +| Module | Description | Scripts | +|--------|-------------|---------| +| **Video Inspection** | Browse raw video frames on HPC without downloading; flags likely-empty videos by motion score. | `notebooks/1_check_videos.ipynb` | +| **QC & Validation** | Read all Nextflow outputs, check videos against QC thresholds (length, pose coverage, tracklet count), flag missing/duplicate entries, post-process gait, and plot fecal boli for manual review. | `r/2_qc_check.R`, `r/2_qc_check_cli.R`, `python/2_qc_check.py` | +| **Data Merging** | Merge the four cleaned feature files (gait, morphometrics, JABS features, fecal boli) with a metadata table; validate that all videos and mice are accounted for across every file. | `r/3_combine_batches.R` | +| **Outlier Detection & Correlation** | Curate the final dataset: remove zero-variance features, detect z-score outliers, generate diagnostic plots, and produce phenotype correlation heatmaps. | `r/4_outliers.R`, `r/5_heatmap.R` | +| **Behavior Review** | Sample short video clips at detected behavior bouts (optionally with pose overlay), then step through them in a browser UI to accept or reject each clip. | `python/6a_qc_classifiers.py`, `python/6b_qc_viewer.py` | +| **Manual Corner to Pose File** | When automated arena-corner detection fails, a human re-labels corners in SLEAP. This script patches the affected pose H5 files with the corrected coordinates. | `python/pose_corner_correction.py` | + +--- + +## Repository Structure + +``` +JABS_nextflow_postprocess/ +├── r/ R pipeline scripts +│ ├── 2_qc_check.R compile QC logs, threshold check, missing/dup (interactive) +│ ├── 2_qc_check_cli.R same as above, CLI version for automation +│ ├── 3_combine_batches.R merge feature files + metadata into unified dataset +│ ├── 4_outliers.R outlier detection and QC figures (project-specific template) +│ ├── 5_heatmap.R phenotype correlation heatmaps +│ ├── render_pose.R utility — render pose overlay videos +│ └── utils.R shared utilities (sourced by other R scripts) +├── python/ Python pipeline scripts +│ ├── 2_qc_check.py compile QC logs, threshold check, missing/dup (CLI) +│ ├── 6a_qc_classifiers.py extract video clips at detected behavior bouts +│ ├── 6b_qc_viewer.py Streamlit app for reviewing and annotating clips +│ └── pose_corner_correction.py utility — embed manual corner corrections into pose H5 files +├── notebooks/ interactive tools (run in JupyterLab) +│ ├── 1_check_videos.ipynb paginated video frame previewer with motion screening +│ └── explore_features.py PCA, clustering, and correlation exploration +├── src/ +│ └── utils.py shared Python utilities (video helpers, pose overlay, motion screening) +├── config/ +│ └── QC_params.yaml default QC thresholds (override via --param flag) +├── pyproject.toml Python dependencies (managed with uv) +└── renv.lock R dependencies (managed with renv) +``` + +Step numbers reflect the recommended order. Utilities (`pose_corner_correction.py`, `utils.R`, `src/utils.py`) are not numbered — they are called by other scripts or used on demand. + +--- + +## Setup + +### R +```r +renv::restore() +``` + +### Python +```bash +uv sync +``` + +--- + +## Scripts + +### 1 — Video Inspection · `notebooks/1_check_videos.ipynb` + +Paginated video frame previewer for HPC environments — no GUI or local download required. Samples 5 evenly spaced frames per video and shows 10 videos per page with Next/Previous navigation. Also includes a motion-based screen to flag likely empty videos. + +Powered by `src/utils.py`. Open in JupyterLab and run cells interactively. + +--- + +### 2 — QC Check · `r/2_qc_check.R` · `r/2_qc_check_cli.R` · `python/2_qc_check.py` + +Reads all Nextflow outputs across batches, validates data completeness, screens for duplicates, post-processes gait data, and generates QC figures for fecal boli. Produces individual cleaned feature files ready for merging. + +**Inputs** — `--input_dir` should contain one or more batch subdirectories with these files (searched recursively): + +| Pattern in filename | Content | +|---------------------|---------| +| `qc_batch_` | QC report CSVs — defines the expected set of videos | +| `gait` | Gait feature CSVs | +| `fecal_boli` | Raw fecal boli CSVs | +| `feature` | JABS behavior feature CSVs | +| `morpho` | Morphometrics CSVs | + +**Assumptions:** +1. Missing gait speed bins mean no gaits were predicted for those bins — not that gait prediction was skipped. +2. All videos to be analyzed appear in at least one `qc_batch_*.csv` file. + +**Running (Python):** +```bash +python python/2_qc_check.py \ + --input_dir /path/to/NextflowOutput \ + --output_dir /path/to/project_output \ + --param config/QC_params.yaml # optional, overrides defaults +``` + +**Running (R CLI):** +```bash +Rscript r/2_qc_check_cli.R \ + --input_dir /path/to/NextflowOutput \ + --output_dir /path/to/project_output \ + --param config/QC_params.yaml +``` + +**QC Parameters (`config/QC_params.yaml`):** + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `expected_length` | 108150 s | Expected video duration | +| `max_tracklet_per_hour` | 6 | Max pose tracklets per hour | +| `max_missing_pose` | 0.5% | Max fraction of missing pose frames | +| `max_missing_segmentation` | 20% | Max fraction of missing segmentation frames | +| `max_missing_keypoint` | 1% | Max fraction of missing keypoint frames | +| `fecal_boli_quantile_plotting` | 0.05 | Lower quantile threshold for fecal boli outlier plots | + +**Outputs:** +``` +output_dir/ +├── final_nextflow_feature_data/ +│ ├── gait_final.csv wide format; stride counts padded with 0 for missing bins; +│ │ variances set to NA for bins with <3 strides +│ ├── morphometrics_final.csv +│ ├── JABS_features_final.csv +│ └── fecal_boli_raw.csv ← requires manual review before merging +└── qc/ + ├── nextflow_qc_logs/ + │ ├── qc_all.csv all videos with pass/fail flags + │ └── qc_failed.csv failed videos only + ├── missing_or_dup_data/ + │ ├── missing_data.csv in QC report but missing from feature files + │ ├── videos_not_in_qc_report.csv + │ └── duplicated_data.xlsx identical or highly correlated rows + └── qc_figs/ + └── fecal_boli_qc_figs.pdf +``` + +**Manual steps after running:** +1. Review `qc/nextflow_qc_logs/qc_failed.csv`. Overlay pose on flagged videos and decide which to keep or exclude. +2. Create `videos_to_exclude.txt` in your output directory — one `NetworkFilename` per line. +3. Review `qc/qc_figs/fecal_boli_qc_figs.pdf`. Correct erroneous counts in `final_nextflow_feature_data/fecal_boli_raw.csv`, then save as `fecal_boli_final.csv` in the same directory. + +--- + +### 3 — Combine Batches · `r/3_combine_batches.R` + +Merges the four cleaned feature files with `metadata.csv`, checks that all videos and mice are represented, and outputs a single merged dataset. + +**Inputs** — expects outputs from step 2 in `final_nextflow_feature_data/`: +- `gait_final.csv`, `morphometrics_final.csv`, `JABS_features_final.csv`, `fecal_boli_final.csv` +- `metadata.csv` — must have a `MouseID` column whose values appear as substrings in `NetworkFilename` +- `videos_to_exclude.txt` — created during manual QC in step 2 + +**Outputs:** +``` +output_dir/ +├── merged_nextflow_dataset.csv +└── qc/missing_or_dup_data/ + ├── NetworkFilenames_missing_in_data.csv + └── mice_missing_in_metadata.csv +``` + +Mismatches between metadata and data do not stop the merge but should be reviewed. + +--- + +### 4 — Outliers · `r/4_outliers.R` + +Template script for final dataset preparation — intended to be customized per project. Removes zero-variance phenotypes, generates z-score outlier plots, and produces the final analysis-ready CSV. + +**Edit the `Set QC and other Values` section to configure:** +- Project name prefix for output files +- Feature substrings to manually exclude +- Z-score threshold for outlier detection +- Number of top outlier mice/phenotypes to plot + +**Inputs:** `merged_nextflow_dataset.csv` from step 3 + +**Outputs:** +``` +output_dir/ +├── YOUR_PROJECT_final_nextflow_dataset.csv +├── features_removed_from_curated_dataset.csv +└── qc/qc_figs/ + ├── scatter_plot_lm_figs.pdf + └── zscore_boxplots/ + ├── gait_boxplot_outlier_figs.pdf + ├── JABS_boxplot_outlier_figs.pdf + ├── morpho_boxplot_outlier_figs.pdf + ├── highest_z_boxplot_outlier_figs.pdf + ├── most_freq_outliers_boxplot_outlier_figs.pdf + └── outlier_videos_summary.csv +``` + +--- + +### 5 — Heatmap · `r/5_heatmap.R` + +Generates phenotype correlation heatmaps. Under active development. + +--- + +### 6a — Behavior Clip Extraction · `python/6a_qc_classifiers.py` + +Samples random video clips at detected behavior bouts, with optional pose skeleton overlay. Reads merged behavior CSV tables produced by Nextflow and writes short MP4 clips for review in step 6b. + +```bash +# Single behavior, no pose overlay: +python python/6a_qc_classifiers.py \ + --behavior-csv NextflowOutput/batch_aa/merged_behavior_tables/merged_escape_bouts_merged.csv \ + --video-dir NextflowOutput/batch_aa/results/videos/ \ + --output-dir /tmp/qc_clips/ \ + --n-clips 3 + +# All behaviors in a folder, with pose skeleton: +python python/6a_qc_classifiers.py \ + --behavior-dir NextflowOutput/batch_aa/merged_behavior_tables/ \ + --video-dir NextflowOutput/batch_aa/results/videos/ \ + --output-dir /tmp/qc_clips/ \ + --n-clips 5 --overlay-pose +``` + +--- + +### 6b — Behavior Clip Viewer · `python/6b_qc_viewer.py` + +Streamlit app for reviewing and annotating the clips produced by `6a_qc_classifiers.py`. Shows one clip at a time with Accept / Reject / Skip buttons; saves verdicts to `annotations.csv`. + +```bash +streamlit run python/6b_qc_viewer.py -- \ + --clips-dir /tmp/qc_clips/ \ + --annotations-csv /tmp/annotations.csv +``` + +--- + +### Utility — Pose Corner Correction · `python/pose_corner_correction.py` + +Copies `pose_est_v6.h5` files and embeds manually corrected corner coordinates from SLEAP annotation files. + +```bash +python python/pose_corner_correction.py \ + --input_dir /path/to/NextflowOutput \ + --output_dir /path/to/pose_v6_dir +``` + +Expects each batch subdirectory to optionally contain: +- `manual_corner_correction.slp` — SLEAP file with corrected corners +- `failed_corners/` — `*_pose_est_v6.h5` files to update + +--- + +## Expected Output File Structure + +``` +/project_output_dir/ +├── videos_to_exclude.txt (manually created) +├── metadata.csv (manually provided) +├── YOUR_PROJECT_final_nextflow_dataset.csv (step 4 output → downstream analysis) +├── features_removed_from_curated_dataset.csv +├── merged_nextflow_dataset.csv (step 3 output) +├── final_nextflow_feature_data/ (step 2 outputs) +│ ├── gait_final.csv +│ ├── morphometrics_final.csv +│ ├── JABS_features_final.csv +│ ├── fecal_boli_raw.csv +│ └── fecal_boli_final.csv (manually corrected) +└── qc/ + ├── nextflow_qc_logs/ + │ ├── qc_all.csv + │ └── qc_failed.csv + ├── missing_or_dup_data/ + │ ├── missing_data.csv + │ ├── videos_not_in_qc_report.csv + │ ├── duplicated_data.xlsx + │ ├── NetworkFilenames_missing_in_data.csv + │ └── mice_missing_in_metadata.csv + └── qc_figs/ + ├── fecal_boli_qc_figs.pdf + ├── scatter_plot_lm_figs.pdf + └── zscore_boxplots/ + ├── gait_boxplot_outlier_figs.pdf + ├── JABS_boxplot_outlier_figs.pdf + ├── morpho_boxplot_outlier_figs.pdf + ├── highest_z_boxplot_outlier_figs.pdf + ├── most_freq_outliers_boxplot_outlier_figs.pdf + └── outlier_videos_summary.csv ``` diff --git a/config/QC_params.yaml b/config/QC_params.yaml new file mode 100644 index 0000000..9812cbe --- /dev/null +++ b/config/QC_params.yaml @@ -0,0 +1,6 @@ +expected_length: 108150 # 60*60*30 + 5*30 <1 hour and 5 seconds, 30 fps> +max_tracklet_per_hour: 6 +max_missing_pose: 0.005 +max_missing_segmentation: 0.2 +max_missing_keypoint: 0.01 +fecal_boli_quantile_plotting: 0.05 diff --git a/notebooks/explore_features.py b/notebooks/explore_features.py new file mode 100644 index 0000000..0f8ee57 --- /dev/null +++ b/notebooks/explore_features.py @@ -0,0 +1,186 @@ +# %% +from pathlib import Path +import pandas as pd +import numpy as np +from sklearn.impute import SimpleImputer +from sklearn.preprocessing import StandardScaler, RobustScaler +from sklearn.decomposition import PCA +import seaborn as sns +import matplotlib.pyplot as plt + +# %% +nextflow_features_dir = "/projects/kumar-lab/USERS/nguyetu/SING-grant/postNextflow/final_nextflow_feature_data" +# %% +feature_dfs = {} +for file in list(Path(nextflow_features_dir).rglob("*.csv")): + feature_df = pd.read_csv(file) + feature_df = feature_df.drop(columns=['nextflow_version']) + feature_dfs[file.stem] = feature_df + +# %% +merged_df = feature_dfs['morphometrics'] \ + .merge(feature_dfs['gait_final'], on="NetworkFilename", how="inner") \ + .merge(feature_dfs['JABS_features_final'], on="NetworkFilename", how="inner") +# %% +# Remove very low variance col +col_var = merged_df.var(numeric_only=True) / (abs(merged_df.mean(numeric_only=True)) + 1e-6) +low_var_cols = col_var[col_var <= 1e-6].index +print(low_var_cols) +filtered_df = merged_df.drop(columns=low_var_cols) +# %% +# Remove columns where more than half the samples have NA +filtered_df = filtered_df.drop(columns = filtered_df.columns[filtered_df.isna().mean() > 0.5]) +# %% +## The only non-numeric column should be NetworkFilename +filtered_df.select_dtypes(exclude="number").columns + +# %% +filtered_df = filtered_df.set_index('NetworkFilename') +filtered_df + +# %% +## Initial PCA +def run_pca_pipeline(df, n_components=None): + # Impute numeric features + num_df = df.select_dtypes(include="number") + imputer = SimpleImputer(strategy="median") + X_imputed = imputer.fit_transform(df) + + # Scale + scaler = StandardScaler() + X_scaled = scaler.fit_transform(X_imputed) + + # PCA + pca = PCA(n_components=n_components) + X_pca = pca.fit_transform(X_scaled) + + # Variance explained by each PC + explained_var = pca.explained_variance_ratio_ + expl_var_dict = {f"PC{i+1}": explained_var[i] for i in range(len(explained_var))} + + # Build PCA dataframe + pca_df = pd.DataFrame(X_pca, + index = df.index, + columns=[f"PC{i+1}" for i in range(X_pca.shape[1])]) + + return pca_df, expl_var_dict + +# %% +pca_df, expl_var_dict = run_pca_pipeline(filtered_df) +pca_df['Sex'] = pca_df.index.map(lambda p: (Path(p).name).split("_")[1].lower()) +pca_df['Strain'] = pca_df.index.map(lambda p: (Path(p).name).split("_")[2]) +pca_df['Batch'] = pca_df.index.map(lambda p: (Path(p).parent.stem)) +pca_df +# %% +hue_cols = ['Sex', 'Strain', 'Batch'] + +fig, axes = plt.subplots(1, 3, figsize=(18,6)) + +for ax, hue in zip(axes, hue_cols): + sns.scatterplot(x = 'PC1', y = 'PC2', hue = hue, data=pca_df, ax = ax) + ax.set_xlabel(f"PC1 ({expl_var_dict['PC1']*100:.1f}%)") + ax.set_ylabel(f"PC2 ({expl_var_dict['PC2']*100:.1f}%)") + +plt.show() + +# %% +## Odd outlier, don't know why yet though +pca_df[pca_df['PC1'] > 200] + +# %% +outlier = "videos/2025-08-20/100594_Female_Cdkl5_trimmed" +excluded_outlier_df = filtered_df.drop(index = outlier) +# %% +pca_df, expl_var_dict = run_pca_pipeline(excluded_outlier_df) +pca_df['Sex'] = pca_df.index.map(lambda p: (Path(p).name).split("_")[1].lower()) +pca_df['Strain'] = pca_df.index.map(lambda p: (Path(p).name).split("_")[2]) +pca_df['Batch'] = pca_df.index.map(lambda p: (Path(p).parent.stem)) +pca_df +# %% +hue_cols = ['Sex', 'Strain', 'Batch'] + +fig, axes = plt.subplots(1, 3, figsize=(18,6)) + +for ax, hue in zip(axes, hue_cols): + sns.scatterplot(x = 'PC1', y = 'PC2', hue = hue, data=pca_df, ax = ax) + ax.set_xlabel(f"PC1 ({expl_var_dict['PC1']*100:.1f}%)") + ax.set_ylabel(f"PC2 ({expl_var_dict['PC2']*100:.1f}%)") + +plt.show() +# %% +half_done_strains = pca_df['Strain'].value_counts()[pca_df['Strain'].value_counts() >=9].index +half_done_strains = ['B6NJ', 'B6J', 'Shank3', 'Ube3a', 'Fmr1', 'Smarcc2'] +half_done_strains_idx = pca_df[pca_df['Strain'].isin(half_done_strains)].index + +within_half_done_df = filtered_df[filtered_df.index.isin(half_done_strains_idx)] + +# %% PCA, just because +pca_df, expl_var_dict = run_pca_pipeline(within_half_done_df) +pca_df['Sex'] = pca_df.index.map(lambda p: (Path(p).name).split("_")[1].lower()) +pca_df['Strain'] = pca_df.index.map(lambda p: (Path(p).name).split("_")[2]) +pca_df['Batch'] = pca_df.index.map(lambda p: (Path(p).parent.stem)) + +hue_cols = ['Sex', 'Strain', 'Batch'] + +fig, axes = plt.subplots(1, 3, figsize=(18,6)) + +for ax, hue in zip(axes, hue_cols): + sns.scatterplot(x = 'PC1', y = 'PC2', hue = hue, data=pca_df, ax = ax) + ax.set_xlabel(f"PC1 ({expl_var_dict['PC1']*100:.1f}%)") + ax.set_ylabel(f"PC2 ({expl_var_dict['PC2']*100:.1f}%)") + +plt.show() +# %% +imputer = SimpleImputer(strategy="median") +df_imputed = imputer.fit_transform(within_half_done_df) +scaler = StandardScaler() +df_scaled = pd.DataFrame( + scaler.fit_transform(df_imputed), + index=within_half_done_df.index, + columns=within_half_done_df.columns +) + +# %% +df_scaled['Strain'] = df_scaled.index.map(lambda p: Path(p).name.split("_")[2]) + +strain_unique = df_scaled['Strain'].unique() +palette = sns.color_palette("tab20", n_colors=len(strain_unique)) +strain_to_color = dict(zip(strain_unique, palette)) + +# Map row colors +row_colors = df_scaled['Strain'].map(strain_to_color) + +# Drop strain from data before clustering +data_for_heatmap = df_scaled.drop(columns=['Strain']) + +sns.clustermap( + data_for_heatmap, + cmap="vlag", + center=0, + figsize=(12,10), + row_colors=row_colors, + yticklabels=False, +) +import matplotlib.patches as mpatches +# Create legend handles +patches = [mpatches.Patch(color=color, label=strain) for strain, color in strain_to_color.items()] + +# Place the legend (outside the heatmap) +plt.legend(handles=patches, bbox_to_anchor=(1, 1), title="Strain") +plt.show() + +# %% +strain_to_color +# %% +features_corr_df = data_for_heatmap.corr() +plt.figure(figsize=(40, 35)) +sns.heatmap(features_corr_df, center=0, cmap='vlag') +# %% +merged_df.shape +# %% +filtered_df.shape +# %% +na_cols = filtered_df.isna().sum().sort_values() +# %% +care_for = ['jumping_bout_behavior' in feat for feat in (na_cols.index)] +na_cols[care_for] \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..aad2f7a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,22 @@ +[project] +name = "jabs-nextflow-postprocess" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "h5py>=3.16.0", + "sleap-io>=0.7.0", + "pandas", + "numpy", + "matplotlib", + "seaborn", + "scikit-learn", + "scipy", + "pyyaml", + "openpyxl", + "opencv-python", + "streamlit", + "ipywidgets", + "ipyfilechooser", +] diff --git a/python/2_qc_check.py b/python/2_qc_check.py new file mode 100644 index 0000000..424ade3 --- /dev/null +++ b/python/2_qc_check.py @@ -0,0 +1,542 @@ +#!/usr/bin/env python3 +""" +A script to clean NextFlow outputs for final processing +The same as "NextFlow_Output_QC_Postprocess_1.R" but to be run in a script +Developed by Dr. Jake Beierle (don't forget the Dr., it's important) + +Documentation: +See comprehensive documentation on the github repository +https://github.com/jacobbeierle/JABS_nextflow_postprocess/tree/main +""" +# %% +import argparse +import os +import sys +from pathlib import Path +import yaml +import pandas as pd +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.backends.backend_pdf import PdfPages +import warnings + +warnings.filterwarnings('ignore') +# %% +def parse_arguments(): + """Parse command-line arguments.""" + parser = argparse.ArgumentParser(description="QC Reporting Pipeline") + + parser.add_argument("--input_dir", type=str, required=True, + help="Input directory (required)") + parser.add_argument("--output_dir", type=str, required=True, + help="Output directory (required)") + parser.add_argument("--param", type=str, default=None, + help="Optional YAML config file to override defaults") + + # QC parameters with defaults + parser.add_argument("--expected_length", type=int, default=60*60*30 + 5*30, + help="Expected video length in seconds [default: 108300]") + parser.add_argument("--max_tracklet_per_hour", type=int, default=6, + help="Maximum tracklets per hour [default: 6]") + parser.add_argument("--max_missing_pose", type=float, default=0.005, + help="Maximum fraction of missing pose [default: 0.005]") + parser.add_argument("--max_missing_segmentation", type=float, default=0.2, + help="Maximum fraction of missing segmentation [default: 0.2]") + parser.add_argument("--max_missing_keypoint", type=float, default=0.01, + help="Maximum fraction of missing keypoints [default: 0.01]") + parser.add_argument("--fecal_boli_quantile_plotting", type=float, default=0.05, + help="Quantile for fecal boli plotting [default: 0.05]") + + return parser.parse_args() + +# %% +def merge_parameters(args): + """Merge parameter priorities from command line and YAML.""" + params = { + 'expected_length': args.expected_length, + 'max_tracklet_per_hour': args.max_tracklet_per_hour, + 'max_missing_pose': args.max_missing_pose, + 'max_missing_segmentation': args.max_missing_segmentation, + 'max_missing_keypoint': args.max_missing_keypoint, + 'fecal_boli_quantile_plotting': args.fecal_boli_quantile_plotting + } + + # Override with YAML if provided + if args.param is not None: + with open(args.param, 'r') as f: + yaml_vals = yaml.safe_load(f) + params.update(yaml_vals) + + return params + +# %% +def create_output_directories(output_dir): + """Create output directory structure.""" + subdirectories = [ + "final_nextflow_feature_data", + "qc/nextflow_qc_logs", + "qc/missing_or_dup_data", + "qc/qc_figs" + ] + + for subdir in subdirectories: + dir_path = os.path.join(output_dir, subdir) + os.makedirs(dir_path, exist_ok=True) + +# %% +def read_raw_data(input_dir, pattern): + """ + Read in multiple CSV files from a directory and harmonize the ID column. + + Args: + input_dir: path to the folder containing CSV files + pattern: string pattern to match file names + + Returns: + A DataFrame with NetworkFilename as the first column + """ + # Find all matching files + files = list(Path(input_dir).rglob(f"*{pattern}*")) + + if not files: + return pd.DataFrame() + + # Read and concatenate all files + dfs = [] + for file in files: + df = pd.read_csv(file) + dfs.append(df) + + raw_data = pd.concat(dfs, ignore_index=True) + + # Use NetworkFilename as the ID column + if "NetworkFilename" not in raw_data.columns: + raw_data.rename(columns={raw_data.columns[0]: "NetworkFilename"}, inplace=True) + + # Move NetworkFilename to first column + cols = raw_data.columns.tolist() + cols.insert(0, cols.pop(cols.index("NetworkFilename"))) + raw_data = raw_data[cols] + + # Clean and harmonize NetworkFilename + raw_data['NetworkFilename'] = (raw_data['NetworkFilename'] + .str.replace('_corrected', '', regex=False) + .str.replace('_filtered', '', regex=False) + .str.replace('.avi', '', regex=False) + .str.replace(r'^\.', '', regex=True) + .str.replace(r'^/', '', regex=True)) + + return raw_data + +# %% +def check_missing_and_dup(expected_videos, data_df, corr_thres=0.99): + """ + Check for missing videos and duplicated rows in a dataset. + + Args: + expected_videos: list of expected NetworkFilename values + data_df: DataFrame containing NetworkFilename column and data + corr_thres: threshold for flagging correlated rows + + Returns: + Dictionary with missing_qc, missing_output, and dup_data + """ + if data_df.empty: + return { + 'missing_qc': [], + 'missing_output': expected_videos, + 'dup_data': pd.DataFrame() + } + + video_missing_output = list(set(expected_videos) - set(data_df['NetworkFilename'])) + video_missing_in_qc = list(set(data_df['NetworkFilename']) - set(expected_videos)) + + # Check for duplicate rows + dup_idx = data_df.iloc[:, 1:].duplicated(keep=False) + + # Check for highly correlated rows + numeric_cols = data_df.select_dtypes(include=[np.number]).columns + if len(numeric_cols) > 0: + numeric_data = data_df[numeric_cols].fillna(0) + if len(numeric_data) > 1: + # Standardize and compute correlation + from scipy.stats import zscore + standardized = numeric_data.apply(zscore, nan_policy='omit') + corr_mat = standardized.T.corr() + + # Find highly correlated pairs + corr_idx = np.zeros(len(data_df), dtype=bool) + for i in range(len(corr_mat)): + for j in range(i+1, len(corr_mat)): + if corr_mat.iloc[i, j] > corr_thres: + corr_idx[i] = True + corr_idx[j] = True + else: + corr_idx = np.zeros(len(data_df), dtype=bool) + else: + corr_idx = np.zeros(len(data_df), dtype=bool) + + # Union of duplicated and correlated rows + all_idx = dup_idx | corr_idx + duplicated_rows = data_df[all_idx] + + return { + 'missing_qc': video_missing_in_qc, + 'missing_output': video_missing_output, + 'dup_data': duplicated_rows + } + +# %% +def process_qc_logs(input_dir, output_dir, params): + """Process and publish QC logs.""" + # Read QC files + qc_files = list(Path(input_dir).rglob("qc_batch_*.csv")) + + if not qc_files: + print("Warning: No QC files found!") + return [] + + qc_logs = [] + for file in qc_files: + df = pd.read_csv(file) + df['QC_file'] = str(file) + qc_logs.append(df) + + qc_log = pd.concat(qc_logs, ignore_index=True) + + # Record why QC failed for each video + qc_log['passed_duration_QC'] = qc_log['video_duration'] == params['expected_length'] + qc_log['passed_tracklet_QC'] = qc_log['pose_tracklets'] < params['max_tracklet_per_hour'] * params['expected_length'] / 108000 + qc_log['passed_segmentation_QC'] = qc_log['seg_counts'] > (1 - params['max_missing_segmentation']) * params['expected_length'] + qc_log['passed_pose_QC'] = qc_log['pose_counts'] > (1 - params['max_missing_pose']) * params['expected_length'] + qc_log['passed_kp_QC'] = qc_log['missing_keypoint_frames'] < params['max_missing_keypoint'] * params['expected_length'] + + # Filter failed QC + passed_cols = [col for col in qc_log.columns if col.startswith('passed_')] + qc_log_failed = qc_log[~qc_log[passed_cols].all(axis=1)] + + # Write QC files + qc_log.to_csv(os.path.join(output_dir, "qc/nextflow_qc_logs/qc_all.csv"), index=False) + qc_log_failed.to_csv(os.path.join(output_dir, "qc/nextflow_qc_logs/qc_failed.csv"), index=False) + + # Get expected videos + expected_videos = (qc_log['video_name'] + .str.replace('_with_fecal_boli', '', regex=False) + .str.replace('_filtered', '', regex=False) + .str.replace(r'^/', '', regex=True) + .unique() + .tolist()) + + return expected_videos + +# %% +def plot_fecal_boli_qc(fecal_boli_raw, output_dir, params): + """Generate fecal boli QC plots.""" + # Pivot data for plotting + value_cols = [col for col in fecal_boli_raw.columns + if col not in ['NetworkFilename', 'nextflow_version']] + + fecal_boli_plot = fecal_boli_raw.melt( + id_vars=['NetworkFilename', 'nextflow_version'], + value_vars=value_cols, + var_name='min', + value_name='fecal_boli' + ).dropna(subset=['fecal_boli']) + + # Extract minute number + fecal_boli_plot['min'] = pd.to_numeric( + fecal_boli_plot['min'].str.extract(r'(\d+)')[0] + ) + + # Create PDF with plots + pdf_path = os.path.join(output_dir, "qc/qc_figs/fecal_boli_qc_figs.pdf") + + with PdfPages(pdf_path) as pdf: + # Plot 1: All mice growth curves + fig, ax = plt.subplots(figsize=(6, 6)) + for name, group in fecal_boli_plot.groupby('NetworkFilename'): + ax.plot(group['min'], group['fecal_boli'], alpha=0.5) + ax.set_xlabel('Time (min)') + ax.set_ylabel('Fecal Boli Count') + ax.set_title('Fecal boli growth, all mice') + pdf.savefig(fig, bbox_inches='tight') + plt.close() + + # Plot 2: Lowest quantile + max_boli = fecal_boli_plot.groupby('NetworkFilename')['fecal_boli'].max() + lowest = max_boli.nsmallest(int(len(max_boli) * params['fecal_boli_quantile_plotting'])) + lowest_data = fecal_boli_plot[fecal_boli_plot['NetworkFilename'].isin(lowest.index)] + + fig, ax = plt.subplots(figsize=(6, 6)) + for name, group in lowest_data.groupby('NetworkFilename'): + ax.plot(group['min'], group['fecal_boli'], alpha=0.5) + ax.set_xlabel('Time (min)') + ax.set_ylabel('Fecal Boli Count') + ax.set_title(f'Lowest {params["fecal_boli_quantile_plotting"]*100}% of fecal boli mice') + pdf.savefig(fig, bbox_inches='tight') + plt.close() + + # Plot 3: Highest quantile + highest = max_boli.nlargest(int(len(max_boli) * params['fecal_boli_quantile_plotting'])) + highest_data = fecal_boli_plot[fecal_boli_plot['NetworkFilename'].isin(highest.index)] + + fig, ax = plt.subplots(figsize=(6, 6)) + for name, group in highest_data.groupby('NetworkFilename'): + ax.plot(group['min'], group['fecal_boli'], alpha=0.5) + ax.set_xlabel('Time (min)') + ax.set_ylabel('Fecal Boli Count') + ax.set_title(f'Highest {params["fecal_boli_quantile_plotting"]*100}% of fecal boli mice') + pdf.savefig(fig, bbox_inches='tight') + plt.close() + + # Plot 4: Histogram of final counts + final_counts = (fecal_boli_plot.sort_values('min', ascending=False) + .drop_duplicates('NetworkFilename')['fecal_boli']) + + fig, ax = plt.subplots(figsize=(6, 6)) + ax.hist(final_counts, bins=range(int(final_counts.min()), int(final_counts.max())+2)) + ax.set_xlabel('Fecal Boli Count') + ax.set_ylabel('Count') + ax.set_title('Fecal boli highest bin, all mice') + pdf.savefig(fig, bbox_inches='tight') + plt.close() + +# %% +def process_gait_data(input_dir, output_dir, expected_videos): + """Process gait data.""" + gait_raw = read_raw_data(input_dir, "gait.csv") + + if gait_raw.empty: + return None, {} + + video_level_metrics = ["Distance Traveled", "Body Length", "Speed", + "Speed Variance", "nextflow_version"] + + # Remove variance measures from speed bins with fewer than 3 strides + variance_cols = [col for col in gait_raw.columns if 'Variance' in col + and col not in video_level_metrics] + + for col in variance_cols: + if 'Stride Count' in gait_raw.columns: + gait_raw.loc[gait_raw['Stride Count'] < 3, col] = np.nan + + # Convert to wide format + id_cols = ['NetworkFilename'] + [col for col in video_level_metrics + if col in gait_raw.columns] + + if 'Speed Bin' in gait_raw.columns: + value_cols = [col for col in gait_raw.columns + if col not in id_cols + ['Speed Bin']] + + gait_wide = gait_raw.pivot_table( + index=id_cols, + columns='Speed Bin', + values=value_cols, + aggfunc='first' + ).reset_index() + + # Flatten column names + gait_wide.columns = ['.'.join(map(str, col)).strip('.') if isinstance(col, tuple) + else col for col in gait_wide.columns] + + # Fill NA stride counts with 0 + stride_cols = [col for col in gait_wide.columns if 'Stride Count' in col] + for col in stride_cols: + gait_wide[col] = gait_wide[col].fillna(0) + else: + gait_wide = gait_raw + + # Check for missing and duplicated data + gait_summary = check_missing_and_dup(expected_videos, gait_wide) + + # Output to CSV + gait_wide.to_csv( + os.path.join(output_dir, "final_nextflow_feature_data/gait_final.csv"), + index=False + ) + + return gait_wide, gait_summary + +# %% +def main(): + """Main execution function.""" + args = parse_arguments() + params = merge_parameters(args) + + input_dir = args.input_dir + output_dir = args.output_dir + + # Print configuration + # Print configuration + print("=== QC CONFIGURATION ===") + print(f"Input directory: {input_dir}") + print(f"Output directory: {output_dir}") + print("QC Parameters:") + for key, value in params.items(): + print(f" {key}: {value}") + + # Create output directories + create_output_directories(output_dir) + + # Process QC logs + expected_videos = process_qc_logs(input_dir, output_dir, params) + + if not expected_videos: + print("Warning: No expected videos found from QC logs!") + expected_videos = [] + + # Process fecal boli data + print("\nProcessing fecal boli data...") + fecal_boli_raw = read_raw_data(input_dir, "fecal_boli.csv") + + if not fecal_boli_raw.empty: + fecal_boli_summary = check_missing_and_dup(expected_videos, fecal_boli_raw) + fecal_boli_raw.to_csv( + os.path.join(output_dir, "final_nextflow_feature_data/fecal_boli_raw.csv"), + index=False + ) + plot_fecal_boli_qc(fecal_boli_raw, output_dir, params) + else: + fecal_boli_summary = {'missing_qc': [], 'missing_output': expected_videos, 'dup_data': pd.DataFrame()} + + # Process gait data + print("Processing gait data...") + gait_wide, gait_summary = process_gait_data(input_dir, output_dir, expected_videos) + + # Process JABS features + print("Processing JABS features...") + jabs_features = read_raw_data(input_dir, "features.csv") + + if not jabs_features.empty: + jabs_summary = check_missing_and_dup(expected_videos, jabs_features) + jabs_features.to_csv( + os.path.join(output_dir, "final_nextflow_feature_data/JABS_features_final.csv"), + index=False + ) + else: + jabs_summary = {'missing_qc': [], 'missing_output': expected_videos, 'dup_data': pd.DataFrame()} + + # Process morphometrics + print("Processing morphometrics...") + morpho_raw = read_raw_data(input_dir, "morphometrics.csv") + + if not morpho_raw.empty: + morpho_summary = check_missing_and_dup(expected_videos, morpho_raw) + morpho_raw.to_csv( + os.path.join(output_dir, "final_nextflow_feature_data/morphometrics_final.csv"), + index=False + ) + else: + morpho_summary = {'missing_qc': [], 'missing_output': expected_videos, 'dup_data': pd.DataFrame()} + + # Report and output warnings + all_missing_data = { + 'fecal_boli': fecal_boli_summary['missing_output'], + 'gait': gait_summary['missing_output'], + 'JABS_features': jabs_summary['missing_output'], + 'Morphometrics': morpho_summary['missing_output'] + } + + videos_not_in_qc_report = { + 'fecal_boli': fecal_boli_summary['missing_qc'], + 'gait': gait_summary['missing_qc'], + 'JABS_features': jabs_summary['missing_qc'], + 'morphometrics': morpho_summary['missing_qc'] + } + + all_duplicated_data = { + 'fecal_boli': fecal_boli_summary['dup_data'], + 'gait': gait_summary['dup_data'], + 'JABS_features': jabs_summary['dup_data'], + 'morphometrics': morpho_summary['dup_data'] + } + + no_missing_output = all(len(v) == 0 for v in all_missing_data.values()) + no_missing_qc = all(len(v) == 0 for v in videos_not_in_qc_report.values()) + no_dups = all(v.empty for v in all_duplicated_data.values()) + + # Write final reports + print("\n=== FINAL ERROR REPORT ===") + + if no_missing_output and no_missing_qc and no_dups: + print("No errors to report") + pd.DataFrame(["No data missing"]).to_csv( + os.path.join(output_dir, "qc/missing_or_dup_data/missing_data.csv"), + index=False, header=False + ) + pd.DataFrame(["No data missing"]).to_csv( + os.path.join(output_dir, "qc/missing_or_dup_data/videos_not_in_qc_report.csv"), + index=False, header=False + ) + pd.DataFrame(["No duplicated data"]).to_csv( + os.path.join(output_dir, "qc/missing_or_dup_data/duplicated_data.csv"), + index=False, header=False + ) + else: + # Report missing output data + if not no_missing_output: + missing_df_list = [] + for output_type, videos in all_missing_data.items(): + if videos: + df = pd.DataFrame({'video_path': videos, output_type: True}) + missing_df_list.append(df) + + if missing_df_list: + missing_df = missing_df_list[0] + for df in missing_df_list[1:]: + missing_df = missing_df.merge(df, on='video_path', how='outer') + missing_df.to_csv( + os.path.join(output_dir, "qc/missing_or_dup_data/missing_data.csv"), + index=False + ) + print("Missing output data for:", ', '.join([k for k, v in all_missing_data.items() if v])) + else: + pd.DataFrame(["No data missing"]).to_csv( + os.path.join(output_dir, "qc/missing_or_dup_data/missing_data.csv"), + index=False, header=False + ) + + # Report missing QC data + if not no_missing_qc: + qc_df_list = [] + for output_type, videos in videos_not_in_qc_report.items(): + if videos: + df = pd.DataFrame({'video_path': videos, output_type: True}) + qc_df_list.append(df) + + if qc_df_list: + qc_df = qc_df_list[0] + for df in qc_df_list[1:]: + qc_df = qc_df.merge(df, on='video_path', how='outer') + qc_df.to_csv( + os.path.join(output_dir, "qc/missing_or_dup_data/videos_not_in_qc_report.csv"), + index=False + ) + print("Missing video in QC for:", ', '.join([k for k, v in videos_not_in_qc_report.items() if v])) + else: + pd.DataFrame(["No data missing"]).to_csv( + os.path.join(output_dir, "qc/missing_or_dup_data/videos_not_in_qc_report.csv"), + index=False, header=False + ) + + # Report duplicated data + if not no_dups: + with pd.ExcelWriter( + os.path.join(output_dir, "qc/missing_or_dup_data/duplicated_data.xlsx"), + engine='openpyxl' + ) as writer: + for name, df in all_duplicated_data.items(): + if not df.empty: + df.to_excel(writer, sheet_name=name[:31], index=False) # Excel sheet name limit + print("Duplicated data for:", ', '.join([k for k, v in all_duplicated_data.items() if not v.empty])) + else: + pd.DataFrame(["No duplicated data"]).to_csv( + os.path.join(output_dir, "qc/missing_or_dup_data/duplicated_data.csv"), + index=False, header=False + ) + + print("\nProcessing complete!") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/python/pose_corner_correction.py b/python/pose_corner_correction.py new file mode 100644 index 0000000..d4a4a45 --- /dev/null +++ b/python/pose_corner_correction.py @@ -0,0 +1,54 @@ +import argparse +import h5py +import sleap_io as sio +from pathlib import Path +import shutil + + +def create_pose_v6(slp_correction, failed_pose_dir, pose_v6_dir): + labels = sio.load_file(slp_correction) + + Path(pose_v6_dir).mkdir(parents=True, exist_ok=True) + for label in labels: + corners = label.instances[0].numpy() + + file_name = Path(label.video.filename[0]).stem + pose_file = failed_pose_dir / f"{file_name}_pose_est_v6.h5" + + if pose_file.exists(): + pose_v6_name = f"{file_name.split('%20')[-1]}_pose_est_v6.h5" + pose_v6_path = Path(pose_v6_dir) / pose_v6_name + shutil.copy2(pose_file, pose_v6_path) + + with h5py.File(pose_v6_path, "r+") as f: + static_objects = f.require_group("static_objects") + + if "corners" in static_objects: + static_objects["corners"][:] = corners + else: + static_objects.create_dataset("corners", data=corners) + + +def main(): + parser = argparse.ArgumentParser( + description="Copy pose_v6 files and embed manually corrected corner coordinates." + ) + parser.add_argument( + "--input_dir", required=True, + help="Nextflow output directory containing batch subdirectories." + ) + parser.add_argument( + "--output_dir", required=True, + help="Destination directory for corrected pose_v6 files." + ) + args = parser.parse_args() + + for batch_dir in Path(args.input_dir).iterdir(): + slp_correction = batch_dir / "manual_corner_correction.slp" + failed_pose_dir = batch_dir / "failed_corners" + if slp_correction.exists(): + create_pose_v6(slp_correction, failed_pose_dir, args.output_dir) + + +if __name__ == "__main__": + main() diff --git a/r/2_qc_check.R b/r/2_qc_check.R new file mode 100644 index 0000000..ab7f402 --- /dev/null +++ b/r/2_qc_check.R @@ -0,0 +1,377 @@ +#!/usr/bin/env Rscript +# A script to clean NextFlow outputs for final processing +# Developed by Dr. Jake Beierle (don't forget the Dr., it's important) + +# ----Documentation---- +# See comprehensive documentation on the github repository +# https://github.com/jacobbeierle/JABS_nextflow_postprocess/tree/main + +# ====================== +# Set up / Libraries / Options +# ====================== + +library(tidyverse) +library(writexl) + +# ======================= +# Argument Configuration +# ====================== + +# Set your paths here: +input.dir <- "~/kumar-group/SING-grant/NextflowOutput/" +output.dir <- "~/kumar-group/SING-grant/postNextflow" + +# Set your parameters here: +params <- list( + expected_length = 60*60*30 + 5*30, + max_tracklet_per_hour = 6, + max_missing_pose = 0.005, + max_missing_segmentation = 0.2, + max_missing_keypoint = 0.01, + fecal_boli_quantile_plotting = 0.05 +) + +# Optional: Override with YAML file (uncomment if you want to use this) +# yaml_vals <- yaml::read_yaml("path/to/your/config.yaml") +# params <- modifyList(params, yaml_vals) + +# Print final configuration +cat("=== QC CONFIGURATION ===\n") +cat("Input directory: ", input.dir, "\n") +cat("Output directory:", output.dir, "\n") +cat("QC Parameters:\n") +print(params) + +# ================== +# Create output directories +# ================== + +for (subdirectory in c("final_nextflow_feature_data", + "qc/nextflow_qc_logs", + "qc/missing_or_dup_data", + "qc/qc_figs")) { + dir.path = file.path(output.dir, subdirectory) + dir.create(dir.path, recursive = T, showWarnings = F) +} + +# ================ +# Process and Publish QC logs with success or failure annotated in a CSV +# ================ +# Read QC files in NextFlow_Output directory +qc_files <- list.files( + path = input.dir, + pattern = "qc_batch_", + full.names = TRUE, + recursive = TRUE +) + +qc_log <- qc_files %>% + set_names(qc_files) %>% + map_dfr(~ read_csv(.x, show_col_types = FALSE), .id = "QC_file") + +# Record why QC failed for each video +qc_log <- qc_log %>% + mutate(passed_duration_QC = video_duration == params$expected_length, + passed_tracklet_QC = pose_tracklets < params$max_tracklet_per_hour * params$expected_length / 108000, + passed_segmentation_QC = seg_counts > (1 - params$max_missing_segmentation) * params$expected_length, + passed_pose_QC = pose_counts > (1 - params$max_missing_pose) * params$expected_length, + passed_kp_QC = missing_keypoint_frames < params$max_missing_keypoint * params$expected_length) + +# Apply thresholds defined above to create a separate 'failed QC' data frame +qc_log.failed <- qc_log %>% + filter(if_any(starts_with("passed_"), ~ !.x)) + +# Write final Nextflow QC files for review by a human +write.csv(qc_log, file.path(output.dir, "qc/nextflow_qc_logs/qc_all.csv"), row.names = FALSE) +write.csv(qc_log.failed, file.path(output.dir, "qc/nextflow_qc_logs/qc_failed.csv"), row.names = FALSE) + +# List of expected videos from the QC log files +expected_videos <- qc_log$video_name |> + gsub("_with_fecal_boli", "", x = _) |> + gsub("_filtered", "", x = _) |> + sub("^/", "", x = _) |> + unique() # Remove duplicate + +# ================== +# Helper functions for processing output data +# ================== +read_raw_data <- function(input_dir, pattern) { + ########################################################################## + # Read in multiple CSV files from a directory and harmonize the ID column + # + # Args: + # input_dir: path to the folder containing CSV files + # pattern: regex pattern to match file names + # + # Returns: + # A tibble with: + # - NetworkFilename as the first column + # - Cleaned NetworkFilename (no "_corrected", "_filtered", ".avi", leading "." or "/") + # + # Notes: + # - If NetworkFilename does not exist, the first column is used as ID + # - Useful for harmonizing output from different workflows before merging + ########################################################################## + + # Read in data from multiple csv files of the same patterns + raw_data <- list.files(path = input_dir, pattern = pattern, + recursive = T, full.names = T) |> + map_dfr(~ read_csv(.x, show_col_types = FALSE)) + + # Use NetworkFilename as the ID column (move it to the first column if not already so) + if ("NetworkFilename" %in% names(raw_data)) { + raw_data <- relocate(raw_data, NetworkFilename, .before = 1) + } else { + colnames(raw_data)[1] <- "NetworkFilename" + } + + # Clean and harmonize the NetworkFilename across data + raw_data$NetworkFilename <- raw_data$NetworkFilename |> + gsub("_corrected", "", x = _) |> + gsub("_filtered", "", x = _) |> + gsub("\\.avi$", "", x = _) |> + sub("^\\.", "", x = _) |> + sub("^/", "", x = _) + + return(raw_data) +} + +check_missing_and_dup <- function(expected_videos, data_df, corr_thres = 0.99) { + ########################################################################## + # Check for missing videos and duplicated rows in a dataset + # + # Args: + # expected_videos: character vector of expected NetworkFilename values + # data_df: dataframe containing a NetworkFilename column and data + # corr_thres: a number for how much correlated 2 rows are to be flagged + # + # Returns: + # A list with three elements: + # - missing_qc: videos present in expected_videos but missing in output_file + # - missing_output: videos present in output_file but missing in expected_videos + # - dup_data: rows in output_file that are duplicated (ignoring NetworkFilename), + # after rounding numeric columns to zero digits + # + # Notes: + # - Useful for QC of experimental datasets, e.g., fecal boli or gait data + # - Rounds numeric columns before checking for duplicates to account for minor differences + ########################################################################## + + video_missing_output <- setdiff(expected_videos, data_df$NetworkFilename) + video_missing_in_qc <- setdiff(data_df$NetworkFilename, expected_videos) + + # Check for rows with identical data (i.e. something went wrong in video recording) + dup_idx <- which(duplicated(data_df[, -1]) | duplicated(data_df[, -1], fromLast = TRUE)) + + # Check for rows with high correlation + corr_mat <- data_df %>% + dplyr::select(where(is.numeric)) %>% + scale() %>% t() %>% + cor(use = "pairwise.complete.obs") + + # correlated row pairs above threshold + row_pairs <- which(corr_mat > corr_thres & row(corr_mat) != col(corr_mat), arr.ind = TRUE) + cor_idx <- unique(c(row_pairs[,1], row_pairs[,2])) + + # UNION: rows that are duplicated OR highly correlated + all_idx <- sort(unique(c(dup_idx, cor_idx))) + + duplicated_rows <- data_df[all_idx,] + + return(list(missing_qc = video_missing_in_qc, + missing_output = video_missing_output, + dup_data = duplicated_rows)) +} + +# ================== +# Process fecal boli data +# ================== +# Concatenate all instances +fecal_boli.raw <- read_raw_data(input_dir = input.dir, + pattern = "fecal_boli.csv") + +# Check for missing and duplicated data +fecal_boli.summary <- check_missing_and_dup(expected_videos = expected_videos, + data_df = fecal_boli.raw) +# Write out all raw, merged fecal boli counts +write.csv(fecal_boli.raw, file.path(output.dir, "final_nextflow_feature_data/fecal_boli_raw.csv"), row.names = FALSE) + +# ================ +# Fecal boli QC plots +# ================ +# Pivot longer to facilitate plotting for QC +fecal_boli.plot <- fecal_boli.raw |> + pivot_longer( + cols = !c(NetworkFilename, nextflow_version), + names_to = "min", + values_to = "fecal_boli", + values_drop_na = TRUE) |> + mutate(min = parse_number(min)) + +# Plot fecal boli QC measures +outFileNamePDF <- file.path(output.dir, "qc/qc_figs/fecal_boli_qc_figs.pdf") +pdf(outFileNamePDF, 6, 6) + +# Growth curve for all mice +ggplot(fecal_boli.plot, aes(min, fecal_boli, group = NetworkFilename, colour = NetworkFilename))+ + geom_line() + + labs(title = "Fecal boli growth, all mice") + + theme(legend.position = "none") + +# Plot mice with lowest fecal boli +fecal_boli.plot |> + summarise(across(fecal_boli, max), .by = NetworkFilename) |> + slice_min(fecal_boli, prop = params$fecal_boli_quantile_plotting) |> + select(NetworkFilename) |> + merge(fecal_boli.plot, by.x = "NetworkFilename") |> + ggplot(aes(min, fecal_boli, group = NetworkFilename, colour = NetworkFilename))+ + geom_line() + + labs(title = paste("Lowest ", params$fecal_boli_quantile_plotting*100, "% of fecal boli mice", sep = "")) + + theme(legend.position = "none") + +# Plot mice with highest fecal boli +fecal_boli.plot |> + summarise(across(fecal_boli, max), .by = NetworkFilename) |> + slice_max(fecal_boli, prop = params$fecal_boli_quantile_plotting) |> + select(NetworkFilename) |> + merge(fecal_boli.plot, by.x = "NetworkFilename") |> + ggplot(aes(min, fecal_boli, group = NetworkFilename, colour = NetworkFilename))+ + geom_line() + + labs(title = paste("Highest ", params$fecal_boli_quantile_plotting*100, "% of fecal boli mice", sep = "")) + + theme(legend.position = "none") + +# Histogram of final fecal boli count +fecal_boli.plot |> + arrange(desc(min)) |> + distinct(NetworkFilename, .keep_all = TRUE) |> + ggplot(aes(fecal_boli))+ + geom_histogram(binwidth = 1, boundary = 0)+ + labs(title = "Fecal boli highest bin, all mice") + + ylab("count") + +invisible(dev.off()) + +# =============== +# Process Gait Data +# =============== +# Import Gait Data +gait.raw <- read_raw_data(input_dir = "~/kumar-group/SING-grant/NextflowOutput/", + pattern = "gait.csv") + +video_level_metrics = c("Distance Traveled", "Body Length", "Speed", "Speed Variance", "nextflow_version") + +gait.wide_format <- gait.raw %>% + # Remove variance measures from speed bins with fewer than 3 strides + mutate(across(.cols = contains("Variance") & !all_of(video_level_metrics), + .fns = ~ ifelse(`Stride Count` < 3, NA, .x))) %>% + # Convert to wide format + pivot_wider(id_cols = c(NetworkFilename, all_of(video_level_metrics)), + names_from = `Speed Bin`, + values_from = -c(NetworkFilename, all_of(video_level_metrics), `Speed Bin`), + names_sep = ".") %>% + mutate(across(.cols = c(`Stride Count.10`, `Stride Count.15`, `Stride Count.20`, `Stride Count.25`), + .fns = ~ replace_na(.x, 0))) + +# Check for missing and duplicated data with gait +gait.summary <- check_missing_and_dup(expected_videos = expected_videos, + data_df = gait.wide_format) + +#output to final CSV +write.csv(gait.wide_format, file.path(output.dir, "final_nextflow_feature_data/gait_final.csv"), row.names = FALSE) + +# ================= +# Process JABS Feature Data +# ================= +JABS.features <- read_raw_data(input_dir = input.dir, + pattern = "features.csv") + +# Check for missing data in JABS.features +JABS.features.summary <- check_missing_and_dup(expected_videos = expected_videos, + data_df = JABS.features) + +# Write the final csv +write.csv(JABS.features, file.path(output.dir, "final_nextflow_feature_data/JABS_features_final.csv"), row.names = FALSE) + +# ==================== +# Process morphometrics feature data +# ==================== +morpho.raw <- read_raw_data(input_dir = input.dir, + pattern = "morphometrics.csv") + +# Check for missing and duplicated data in morphometric outputs +morpho.summary <- check_missing_and_dup(expected_videos = expected_videos, + data_df = morpho.raw) + +#output to final CSV +write.csv(morpho.raw, file.path(output.dir, "final_nextflow_feature_data/morphometrics.csv"), row.names = FALSE) + +# ================= +# Report and output data for all warnings +# ================= +# Videos in QC but not in output +all_missing_data <- list("fecal_boli" = fecal_boli.summary$missing_output, + "gait" = gait.summary$missing_output, + "JABS_features" = JABS.features.summary$missing_output, + "Morphometrics" = morpho.summary$missing_output) + +# Videos in output but not in QC +videos_not_in_qc_report <- list("fecal_boli" = fecal_boli.summary$missing_qc, + "gait" = gait.summary$missing_qc, + "JABS_features" = JABS.features.summary$missing_qc, + "morphometrics" = morpho.summary$missing_qc) + +# Output data that is duplicated in the data frames +all_duplicated_data <- list("fecal_boli" = fecal_boli.summary$dup_data, + "gait" = gait.summary$dup_data, + "JABS_features" = JABS.features.summary$dup_data, + "morphometrics" = morpho.summary$dup_data) + +no_missing_output <- all(sapply(all_missing_data, length) == 0) +no_missing_qc <- all(sapply(videos_not_in_qc_report, length) == 0) +no_dups <- all(sapply(all_duplicated_data, nrow) == 0) + +# Summarize and write warnings +cat("=== FINAL ERROR REPORT ===\n") +if (no_missing_output && no_missing_qc && no_dups) { + cat("No errors to report\n") + # Create placeholder files + write.csv("No data missing", file.path(output.dir, "qc/missing_or_dup_data/missing_data.csv"), row.names = FALSE) + write.csv("No data missing", file.path(output.dir, "qc/missing_or_dup_data/videos_not_in_qc_report.csv"), row.names = FALSE) + write_xlsx(as.data.frame("No duplicated data"), file.path(output.dir, "qc/missing_or_dup_data/duplicated_data.xlsx"), row.names = FALSE) +} else { + + # Check for missing output data + if (no_missing_output) { + write.csv("No data missing", file.path(output.dir, "qc/missing_or_dup_data/missing_data.csv"), row.names = FALSE) + } else { + all_missing_data <- all_missing_data %>% + enframe(., name = "outputType", value = "video_path") %>% + unnest(cols = video_path) %>% + mutate(missing = TRUE) %>% + pivot_wider(id_cols = video_path, names_from = outputType, values_from = missing) + write.csv(all_missing_data, file.path(output.dir, "qc/missing_or_dup_data/missing_data.csv"), row.names = FALSE) + cat(paste("Missing", colnames(all_missing_data)[-1], "data"), sep = "\n") + } + + # Check for missing QC data + if (no_missing_qc) { + write.csv("No data missing", file.path(output.dir, "qc/missing_or_dup_data/videos_not_in_qc_report.csv"), row.names = FALSE) + } else { + videos_not_in_qc_report <- videos_not_in_qc_report %>% + enframe(., name = "outputType", value = "video_path") %>% + unnest(cols = video_path) %>% + mutate(missing = TRUE) %>% + pivot_wider(id_cols = video_path, names_from = outputType, values_from = missing) + write.csv(videos_not_in_qc_report, file.path(output.dir, "qc/missing_or_dup_data/videos_not_in_qc_report.csv"), row.names = FALSE) + cat(paste("Missing video in QC for", colnames(videos_not_in_qc_report)[-1], "data"), sep = "\n") + } + + # Check for duplicated data + if (no_dups) { + write_xlsx(as.data.frame("No duplicated data"), file.path(output.dir, "qc/missing_or_dup_data/duplicated_data.xlsx"), row.names = FALSE) + } else { + write_xlsx(all_duplicated_data, path = file.path(output.dir, "qc/missing_or_dup_data/duplicated_data.xlsx")) + cat(paste("Duplicated data for", names(all_duplicated_data)[sapply(all_duplicated_data, nrow) != 0]), sep = "\n") + } +} + diff --git a/r/2_qc_check_cli.R b/r/2_qc_check_cli.R new file mode 100644 index 0000000..f44244f --- /dev/null +++ b/r/2_qc_check_cli.R @@ -0,0 +1,414 @@ +#!/usr/bin/env Rscript +# A script to clean NextFlow outputs for final processing +# The same as "NextFlow_Output_QC_Postprocess_1.R" but to be run in a script +# Developed by Dr. Jake Beierle (don't forget the Dr., it's important) + +# ----Documentation---- +# See comprehensive documentation on the github repository +# https://github.com/jacobbeierle/JABS_nextflow_postprocess/tree/main + +# ====================== +# Set up / Libraries / Options +# ====================== + +suppressPackageStartupMessages({ + library(optparse) + library(yaml) + library(tidyverse) + library(writexl) +}) + +# ======================= +# Argument Parser +# ====================== + +option_list <- list( + make_option("--input_dir", type = "character", help = "Input directory (required)"), + make_option("--output_dir", type = "character", help = "Output directory (required)"), + make_option("--param", type = "character", default = NULL, + help = "Optional YAML config file to override defaults"), + + # QC parameters with defaults + make_option("--expected_length", type = "integer", default = 60*60*30 + 5*30, + help = "Expected video length in seconds [default %default]"), + make_option("--max_tracklet_per_hour", type = "integer", default = 6, + help = "Maximum tracklets per hour [default %default]"), + make_option("--max_missing_pose", type = "double", default = 0.005, + help = "Maximum fraction of missing pose [default %default]"), + make_option("--max_missing_segmentation", type = "double", default = 0.2, + help = "Maximum fraction of missing segmentation [default %default]"), + make_option("--max_missing_keypoint", type = "double", default = 0.01, + help = "Maximum fraction of missing keypoints [default %default]"), + make_option("--fecal_boli_quantile_plotting", type = "double", default = 0.05, + help = "Quantile for fecal boli plotting [default %default]") +) + +opt_parser <- OptionParser(option_list = option_list, + description = "QC Reporting Pipeline") + +# Parse command-line arguments +args <- parse_args(opt_parser) + +# Check for required arguments +if (is.null(opt$input_dir) || is.null(opt$output_dir)) { + print_help(parser) + stop("Both --input_dir and --output_dir are required.", call. = FALSE) +} + +# ======================= +# Merge parameter priorites +# ====================== + +# Set input, output directories +input.dir <- args$input_dir +output.dir <- args$output_dir + +# Set defaults +params <- list( + expected_length = args$expected_length, + max_tracklet_per_hour = args$max_tracklet_per_hour, + max_missing_pose = args$max_missing_pose, + max_missing_segmentation = args$max_missing_segmentation, + max_missing_keypoint = args$max_missing_keypoint, + fecal_boli_quantile_plotting = args$fecal_boli_quantile_plotting +) + +# Override with YAML (if provided) +if (!is.null(args$param)) { + yaml_vals <- yaml::read_yaml(args$param) + params <- modifyList(params, yaml_vals) +} + +# Print final configuration +cat("=== QC CONFIGURATION ===\n") +cat("Input directory: ", input.dir, "\n") +cat("Output directory:", output.dir, "\n") +cat("QC Parameters:\n") +print(params) + +# ================== +# Create output directories +# ================== + +for (subdirectory in c("final_nextflow_feature_data", + "qc/nextflow_qc_logs", + "qc/missing_or_dup_data", + "qc/qc_figs")) { + dir.path = file.path(output.dir, subdirectory) + dir.create(dir.path, recursive = T, showWarnings = F) +} + +# ================ +# Process and Publish QC logs with success or failure annotated in a CSV +# ================ +# Read QC files in NextFlow_Output directory +qc_log <- list.files( + path = input.dir, + pattern = "qc_batch_", + full.names = TRUE, + recursive = TRUE + ) %>% + read_csv(id = "QC_file", show_col_types = FALSE) + +# Record why QC failed for each video +qc_log <- qc_log %>% + mutate(passed_duration_QC = video_duration == params$expected_length, + passed_tracklet_QC = pose_tracklets < params$max_tracklet_per_hour * params$expected_length / 108000, + passed_segmentation_QC = seg_counts > (1 - params$max_missing_segmentation) * params$expected_length, + passed_pose_QC = pose_counts > (1 - params$max_missing_pose) * params$expected_length, + passed_kp_QC = missing_keypoint_frames < params$max_missing_keypoint * params$expected_length) + +# Apply thresholds defined above to create a separate 'failed QC' data frame +qc_log.failed <- qc_log %>% + filter(if_any(starts_with("passed_"), ~ !.x)) + +# Write final Nextflow QC files for review by a human +write.csv(qc_log, file.path(output.dir, "qc/nextflow_qc_logs/qc_all.csv"), row.names = FALSE) +write.csv(qc_log.failed, file.path(output.dir, "qc/nextflow_qc_logs/qc_failed.csv"), row.names = FALSE) + +# List of expected videos from the QC log files +expected_videos <- qc_log$video_name |> + gsub("_with_fecal_boli", "", x = _) |> + gsub("_filtered", "", x = _) |> + sub("^/", "", x = _) |> + unique() # Remove duplicate + +# ================== +# Helper functions for processing output data +# ================== +read_raw_data <- function(input_dir, pattern) { + ########################################################################## + # Read in multiple CSV files from a directory and harmonize the ID column + # + # Args: + # input_dir: path to the folder containing CSV files + # pattern: regex pattern to match file names + # + # Returns: + # A tibble with: + # - NetworkFilename as the first column + # - Cleaned NetworkFilename (no "_corrected", "_filtered", ".avi", leading "." or "/") + # + # Notes: + # - If NetworkFilename does not exist, the first column is used as ID + # - Useful for harmonizing output from different workflows before merging + ########################################################################## + + # Read in data from multiple csv files of the same patterns + raw_data <- list.files(path = input_dir, pattern = pattern, + recursive = T, full.names = T) |> + read_csv(show_col_types = FALSE) + + # Use NetworkFilename as the ID column (move it to the first column if not already so) + if ("NetworkFilename" %in% names(raw_data)) { + raw_data <- relocate(raw_data, NetworkFilename, .before = 1) + } else { + colnames(raw_data)[1] <- "NetworkFilename" + } + + # Clean and harmonize the NetworkFilename across data + raw_data$NetworkFilename <- raw_data$NetworkFilename |> + gsub("_corrected", "", x = _) |> + gsub("_filtered", "", x = _) |> + gsub("\\.avi$", "", x = _) |> + sub("^\\.", "", x = _) |> + sub("^/", "", x = _) + + return(raw_data) +} + +check_missing_and_dup <- function(expected_videos, data_df, corr_thres = 0.99) { + ########################################################################## + # Check for missing videos and duplicated rows in a dataset + # + # Args: + # expected_videos: character vector of expected NetworkFilename values + # data_df: dataframe containing a NetworkFilename column and data + # corr_thres: a number for how much correlated 2 rows are to be flagged + # + # Returns: + # A list with three elements: + # - missing_qc: videos present in expected_videos but missing in output_file + # - missing_output: videos present in output_file but missing in expected_videos + # - dup_data: rows in output_file that are duplicated (ignoring NetworkFilename), + # after rounding numeric columns to zero digits + # + # Notes: + # - Useful for QC of experimental datasets, e.g., fecal boli or gait data + # - Rounds numeric columns before checking for duplicates to account for minor differences + ########################################################################## + + video_missing_output <- setdiff(expected_videos, data_df$NetworkFilename) + video_missing_in_qc <- setdiff(data_df$NetworkFilename, expected_videos) + + # Check for rows with identical data (i.e. something went wrong in video recording) + dup_idx <- which(duplicated(data_df[, -1]) | duplicated(data_df[, -1], fromLast = TRUE)) + + # Check for rows with high correlation + corr_mat <- data_df %>% + dplyr::select(where(is.numeric)) %>% + scale() %>% t() %>% + cor(use = "pairwise.complete.obs") + + # correlated row pairs above threshold + row_pairs <- which(corr_mat > corr_thres & row(corr_mat) != col(corr_mat), arr.ind = TRUE) + cor_idx <- unique(c(row_pairs[,1], row_pairs[,2])) + + # UNION: rows that are duplicated OR highly correlated + all_idx <- sort(unique(c(dup_idx, cor_idx))) + + duplicated_rows <- data_df[all_idx,] + + return(list(missing_qc = video_missing_in_qc, + missing_output = video_missing_output, + dup_data = duplicated_rows)) +} + +# ================== +# Process fecal boli data +# ================== +# Concatenate all instances +fecal_boli.raw <- read_raw_data(input_dir = input.dir, + pattern = "fecal_boli.csv") + +# Check for missing and duplicated data +fecal_boli.summary <- check_missing_and_dup(expected_videos = expected_videos, + data_df = fecal_boli.raw) +# Write out all raw, merged fecal boli counts +write.csv(fecal_boli.raw, file.path(output.dir, "final_nextflow_feature_data/fecal_boli_raw.csv"), row.names = FALSE) + +# ================ +# Fecal boli QC plots +# ================ +# Pivot longer to facilitate plotting for QC +fecal_boli.plot <- fecal_boli.raw |> + pivot_longer( + cols = !c(NetworkFilename, nextflow_version), + names_to = "min", + values_to = "fecal_boli", + values_drop_na = TRUE) |> + mutate(min = parse_number(min)) + +# Plot fecal boli QC measures +outFileNamePDF <- file.path(output.dir, "qc/qc_figs/fecal_boli_qc_figs.pdf") +pdf(outFileNamePDF, 6, 6) + +# Growth curve for all mice +ggplot(fecal_boli.plot, aes(min, fecal_boli, group = NetworkFilename, colour = NetworkFilename))+ + geom_line() + + labs(title = "Fecal boli growth, all mice") + + theme(legend.position = "none") + +# Plot mice with lowest fecal boli +fecal_boli.plot |> + summarise(across(fecal_boli, max), .by = NetworkFilename) |> + slice_min(fecal_boli, prop = params$fecal_boli_quantile_plotting) |> + select(NetworkFilename) |> + merge(fecal_boli.plot, by.x = "NetworkFilename") |> + ggplot(aes(min, fecal_boli, group = NetworkFilename, colour = NetworkFilename))+ + geom_line() + + labs(title = paste("Lowest ", params$fecal_boli_quantile_plotting*100, "% of fecal boli mice", sep = "")) + + theme(legend.position = "none") + +# Plot mice with highest fecal boli +fecal_boli.plot |> + summarise(across(fecal_boli, max), .by = NetworkFilename) |> + slice_max(fecal_boli, prop = params$fecal_boli_quantile_plotting) |> + select(NetworkFilename) |> + merge(fecal_boli.plot, by.x = "NetworkFilename") |> + ggplot(aes(min, fecal_boli, group = NetworkFilename, colour = NetworkFilename))+ + geom_line() + + labs(title = paste("Highest ", params$fecal_boli_quantile_plotting*100, "% of fecal boli mice", sep = "")) + + theme(legend.position = "none") + +# Histogram of final fecal boli count +fecal_boli.plot |> + arrange(desc(min)) |> + distinct(NetworkFilename, .keep_all = TRUE) |> + ggplot(aes(fecal_boli))+ + geom_histogram(binwidth = 1, boundary = 0)+ + labs(title = "Fecal boli highest bin, all mice") + + ylab("count") + +invisible(dev.off()) + +# =============== +# Process Gait Data +# =============== +# Import Gait Data +gait.raw <- read_raw_data(input_dir = input.dir, + pattern = "gait.csv") + +video_level_metrics = c("Distance Traveled", "Body Length", "Speed", "Speed Variance", "nextflow_version") + +gait.wide_format <- gait.raw %>% + # Remove variance measures from speed bins with fewer than 3 strides + mutate(across(.cols = contains("Variance") & !all_of(video_level_metrics), + .fns = ~ ifelse(`Stride Count` < 3, NA, .x))) %>% + # Convert to wide format + pivot_wider(id_cols = c(NetworkFilename, all_of(video_level_metrics)), + names_from = `Speed Bin`, + values_from = -c(NetworkFilename, all_of(video_level_metrics), `Speed Bin`), + names_sep = ".") %>% + mutate(across(.cols = c(`Stride Count.10`, `Stride Count.15`, `Stride Count.20`, `Stride Count.25`), + .fns = ~ replace_na(.x, 0))) + +# Check for missing and duplicated data with gait +gait.summary <- check_missing_and_dup(expected_videos = expected_videos, + data_df = gait.wide_format) + +#output to final CSV +write.csv(gait.wide_format, file.path(output.dir, "final_nextflow_feature_data/gait_final.csv"), row.names = FALSE) + +# ================= +# Process JABS Feature Data +# ================= +JABS.features <- read_raw_data(input_dir = "~/kumar-group/SING-grant/NextflowOutput/", + pattern = "features.csv") + +# Check for missing data in JABS.features +JABS.features.summary <- check_missing_and_dup(expected_videos = expected_videos, + data_df = JABS.features) + +# Write the final csv +write.csv(JABS.features, file.path(output.dir, "final_nextflow_feature_data/JABS_features_final.csv"), row.names = FALSE) + +# ==================== +# Process morphometrics feature data +# ==================== +morpho.raw <- read_raw_data(input_dir = "~/kumar-group/SING-grant/NextflowOutput/", + pattern = "morphometrics.csv") + +# Check for missing and duplicated data in morphometric outputs +morpho.summary <- check_missing_and_dup(expected_videos = expected_videos, + data_df = morpho.raw) + +# ================= +# Report and output data for all warnings +# ================= +# Videos in QC but not in output +all_missing_data <- list("fecal_boli" = fecal_boli.summary$missing_output, + "gait" = gait.summary$missing_output, + "JABS_features" = JABS.features.summary$missing_output, + "Morphometrics" = morpho.summary$missing_output) + +# Videos in output but not in QC +videos_not_in_qc_report <- list("fecal_boli" = fecal_boli.summary$missing_qc, + "gait" = gait.summary$missing_qc, + "JABS_features" = JABS.features.summary$missing_qc, + "morphometrics" = morpho.summary$missing_qc) + +# Output data that is duplicated in the data frames +all_duplicated_data <- list("fecal_boli" = fecal_boli.summary$dup_data, + "gait" = gait.summary$dup_data, + "JABS_features" = JABS.features.summary$dup_data, + "morphometrics" = morpho.summary$dup_data) + +no_missing_output <- all(sapply(all_missing_data, length) == 0) +no_missing_qc <- all(sapply(videos_not_in_qc_report, length) == 0) +no_dups <- all(sapply(all_duplicated_data, nrow) == 0) + +# Summarize and write warnings +cat("=== FINAL ERROR REPORT ===\n") +if (no_missing_output && no_missing_qc && no_dups) { + cat("No errors to report\n") + # Create placeholder files + write.csv("No data missing", file.path(output.dir, "qc/missing_or_dup_data/missing_data.csv"), row.names = FALSE) + write.csv("No data missing", file.path(output.dir, "qc/missing_or_dup_data/videos_not_in_qc_report.csv"), row.names = FALSE) + write_xlsx(as.data.frame("No duplicated data"), file.path(output.dir, "qc/missing_or_dup_data/duplicated_data.xlsx"), row.names = FALSE) +} else { + + # Check for missing output data + if (no_missing_output) { + write.csv("No data missing", file.path(output.dir, "qc/missing_or_dup_data/missing_data.csv"), row.names = FALSE) + } else { + all_missing_data <- all_missing_data %>% + enframe(., name = "outputType", value = "video_path") %>% + unnest(cols = video_path) %>% + mutate(missing = TRUE) %>% + pivot_wider(id_cols = video_path, names_from = outputType, values_from = missing) + write.csv(all_missing_data, file.path(output.dir, "qc/missing_or_dup_data/missing_data.csv"), row.names = FALSE) + cat(paste("Missing", colnames(all_missing_data)[-1], "data"), sep = "\n") + } + + # Check for missing QC data + if (no_missing_qc) { + write.csv("No data missing", file.path(output.dir, "qc/missing_or_dup_data/videos_not_in_qc_report.csv"), row.names = FALSE) + } else { + videos_not_in_qc_report <- videos_not_in_qc_report %>% + enframe(., name = "outputType", value = "video_path") %>% + unnest(cols = video_path) %>% + mutate(missing = TRUE) %>% + pivot_wider(id_cols = video_path, names_from = outputType, values_from = missing) + write.csv(videos_not_in_qc_report, file.path(output.dir, "qc/missing_or_dup_data/videos_not_in_qc_report.csv"), row.names = FALSE) + cat(paste("Missing video in QC for", colnames(videos_not_in_qc_report)[-1], "data"), sep = "\n") + } + + # Check for duplicated data + if (no_dups) { + write_xlsx(as.data.frame("No duplicated data"), file.path(output.dir, "qc/missing_or_dup_data/duplicated_data.xlsx"), row.names = FALSE) + } else { + write_xlsx(all_duplicated_data, path = file.path(output.dir, "qc/missing_or_dup_data/duplicated_data.xlsx")) + cat(paste("Duplicated data for", names(all_duplicated_data)[sapply(all_duplicated_data, nrow) != 0]), sep = "\n") + } +} diff --git a/merge_all_dataframes_2.R b/r/3_combine_batches.R similarity index 100% rename from merge_all_dataframes_2.R rename to r/3_combine_batches.R diff --git a/final_dataframe_QC_3.R b/r/4_outliers.R similarity index 75% rename from final_dataframe_QC_3.R rename to r/4_outliers.R index a5d9481..ad79343 100644 --- a/final_dataframe_QC_3.R +++ b/r/4_outliers.R @@ -16,12 +16,9 @@ options(error = NULL) #helps with error handling in functions checking for direc #If you are not using an R project, set your working directory #working.directory <- "C:\\Users\\beierj\\Desktop\\2025-04-09_NTG_C1-C5_Analysis" -working.directory <- "C:\\Users\\beierj\\Desktop\\2025-10-29_OW_Pilot_V1-5_WS1-4" - ## Set some pithy project name to be appended to your curated dataset #project_name <- "NTG_C1-5" -project_name <- "OW" #Sometimes features from nextflow are not useful because they are not reliable in your experimental @@ -32,14 +29,14 @@ project_name <- "OW" #These strings will be used to filter out cols that contain them, so be specific in your choice of wording #features.removed.manually <- c("Side_seizure", "Tail_jerk", "Wild_jumping") -features.removed.manually <- NULL +#features.removed.manually <- NULL #Set z-score threshold for outlier screening zscore.threshold <- 6 #Set the number of phenotypes to print with the highest, and most common outliers how.many.highest.zscore.outlier.plots <- 16 -how.many.most.freq.outlier.plots <- 16 +how.many.most.freq.outlier.plots <- 8 #Create functions-------------------------------------------------------------- @@ -303,7 +300,7 @@ if(!dir.exists(qc.figs.dir)){ check_files_exist(file.path(merged_nextflow_dataset.dir, "merged_nextflow_dataset.csv")) #Load merged dataset and set variables for final data output---- -data.nextflow <- read_csv(file.path(merged_nextflow_dataset.dir, "merged_nextflow_dataset.csv")) +data.Nextflow <- read_csv(file.path(merged_nextflow_dataset.dir, "merged_nextflow_dataset.csv")) #Check that qc directory for missing and duplicated data exists, report error if it does not @@ -319,22 +316,22 @@ if(!dir.exists(qc.figs.dir)){ #Manually remove the features you defined at the top of this script if(length(features.removed.manually)){ - data.nextflow <- data.nextflow |> + data.Nextflow <- data.Nextflow |> select(!contains(features.removed.manually)) } #Some of the morphometric features are always identical, and I remove these now too. # Identify columns where the variance is not zero -nonzero_variance_cols <- sapply(data.nextflow, function(x) var(x, na.rm = TRUE)) == 0 +nonzero_variance_cols <- sapply(data.Nextflow, function(x) var(x, na.rm = TRUE)) == 0 #Replace the NAs with FALSE, as these represent strings etc nonzero_variance_cols[is.na(nonzero_variance_cols)] <- FALSE #Recording these features in a vector makes reporting what was removed easier at the end -features.removed.zerovar <- colnames(data.nextflow)[nonzero_variance_cols] +features.removed.zerovar <- colnames(data.Nextflow)[nonzero_variance_cols] # Subset the data frame to remove zerovar cols -data.nextflow <- data.nextflow[, !nonzero_variance_cols] +data.Nextflow <- data.Nextflow[, !nonzero_variance_cols] #Print a csv of the features removed with reasons @@ -363,22 +360,22 @@ if(nrow(features.removed.all)){ ##Provide string of day for analysis -#data.nextflow$Day <- str_split_i(data.nextflow$NetworkFilename, "/", i=3) -#data.nextflow$Day <- gsub("D", "", data.nextflow$Day) -#data.nextflow$Day <- as.numeric(data.nextflow$Day) +#data.Nextflow$Day <- str_split_i(data.Nextflow$NetworkFilename, "/", i=3) +#data.Nextflow$Day <- gsub("D", "", data.Nextflow$Day) +#data.Nextflow$Day <- as.numeric(data.Nextflow$Day) ##I'm removing doses and days I don't really care about, i.e. 2.5 and 5 mg/kg -#data.nextflow <- subset(data.nextflow, data.nextflow$Tx!= 5 & data.nextflow$Tx != 2.5) -#data.nextflow <- subset(data.nextflow, data.nextflow$Day != 21) +#data.Nextflow <- subset(data.Nextflow, data.Nextflow$Tx!= 5 & data.Nextflow$Tx != 2.5) +#data.Nextflow <- subset(data.Nextflow, data.Nextflow$Day != 21) ##Reorder the factors -#data.nextflow <- relocate(data.nextflow, NetworkFilename, FileName, MouseID, PenID, ExptNumber, Cohort, Sex = sex, Day, Tx, LL) +#data.Nextflow <- relocate(data.Nextflow, NetworkFilename, FileName, MouseID, PenID, ExptNumber, Cohort, Sex = sex, Day, Tx, LL) ########################################Summary information reporting-------------------------------------------------- #You can also take this time to report useful information for your analysis #Example generating summaries of number of samples/timepoint -#data.nextflow |> +#data.Nextflow |> # group_by(Day, Tx) |> # summarise(N = n()) |> # arrange(desc(Tx), Day) |> @@ -386,12 +383,49 @@ if(nrow(features.removed.all)){ #Publish the final, curated data set-------------------------------------------- -write.csv(data.nextflow, paste0(project_name, "_final_nextflow_dataset.csv"), row.names = FALSE) +write.csv(data.Nextflow, paste0(project_name, "_final_nextflow_dataset.csv"), row.names = FALSE) #Before QC plotting, I use janitor to clean col names because Nextflow col names are rough to work with---- -colnames(data.nextflow) <- colnames(clean_names(data.nextflow)) +colnames(data.Nextflow) <- colnames(clean_names(data.Nextflow)) + +#Preliminary QC figures: outlines from linear model of 2 phenotypes------------- + +#Publish figures of linear relationships between 2 variables, with the 5 most distant points labeled +#see doc in function section for details +pdf(file.path(qc.figs.dir, "scatter_plot_lm_figs.pdf"), 7,7) +print( + qc_plot_lm_outliers(data.Nextflow$distance_traveled, + data.Nextflow$bin_avg_55_locomotion_distance_cm, + data.Nextflow$network_filename) +) -#Preliminary QC: Read in gait colnames for subsetting data plots----- +print( + qc_plot_lm_outliers(data.Nextflow$bin_sum_55_jumping_bout_behavior, + data.Nextflow$bin_sum_55_escape_bout_behavior, + data.Nextflow$network_filename) +) + +print(qc_plot_lm_outliers(data.Nextflow$bin_sum_55_freeze_bout_behavior, + data.Nextflow$bin_sum_55_freezing_bout_behavior, + data.Nextflow$network_filename) +) + +print(qc_plot_lm_outliers(data.Nextflow$distance_traveled, + data.Nextflow$speed, + data.Nextflow$network_filename) +) + +print(qc_plot_lm_outliers(data.Nextflow$bin_sum_55_in_periphery_time_secs, + data.Nextflow$bin_sum_55_in_corner_time_secs, + data.Nextflow$network_filename) +) +dev.off() + + +#Preliminary QC figures: checking for outliers using z-score----- +#Subset datasets and create Network_filename + phenotypes + +#Read in gait colnames for subsetting outlier data plots gait.cols <- list.files( path = merged_nextflow_dataset.dir, pattern = "gait_final", @@ -418,145 +452,8 @@ JABS.cols <- list.files( clean_names()|> colnames() - - -#Preliminary QC figures: JABS phenotypes over time, average and by mouse------------- - -#By time - -pdf(file.path(qc.figs.dir, "JABS_features_over_timepoints.pdf") , 15,15) - -p1 <- data.nextflow|> - select(contains(c("time", "network")) & !contains(c("traveled", "in_corner", "periphery", "locomotion"))) |> - select(starts_with("bin_sum_") | contains("network")) |> - pivot_longer( - cols = starts_with("bin_sum_"), - names_to = "bin_size", - values_to = "count" - ) |> - extract( - bin_size, - into = c("bin_size", "behavior"), - regex = "bin_sum_(\\d+)_([\\S]+)_time_secs", - convert = TRUE - ) |> - ggplot(aes(x = bin_size, y = count)) + - geom_line(aes(group = network_filename, colour = network_filename)) + - geom_point(aes(group = network_filename, colour = network_filename)) + - stat_summary(geom = "line", fun.data = mean_se) + - stat_summary(geom = "point", fun.data = mean_se) + - stat_summary(geom = "ribbon", fun.data = mean_se, alpha = 0.3, colour = "black") + - facet_wrap(facets = vars(behavior), scales = "free_y") + - labs(title = "Summed time in behavior over 2.5hrs", - y = "seconds (?)", - x = "mins") + - theme(legend.position = "none") -print(p1) - -#By bout -p1 <- data.nextflow|> - select(contains(c("bout", "network")) & !contains(c("traveled", "in_corner", "periphery", "locomotion"))) |> - select(starts_with("bin_sum_") | contains("network")) |> - pivot_longer( - cols = starts_with("bin_sum_"), - names_to = "bin_size", - values_to = "count" - ) |> - extract( - bin_size, - into = c("bin_size", "behavior"), - regex = "bin_sum_(\\d+)_([\\S]+)_bout_behavior", - convert = TRUE - ) |> - ggplot(aes(x = bin_size, y = count)) + - geom_line(aes(group = network_filename, colour = network_filename)) + - geom_point(aes(group = network_filename, colour = network_filename)) + - stat_summary(geom = "line", fun.data = mean_se) + - stat_summary(geom = "ribbon", alpha = 0.3) + - stat_summary(geom = "point", fun.data = mean_se) + - facet_wrap(facets = vars(behavior), scales = "free_y") + - labs(title = "Bouts of behavior", - y = "bouts", - x = "mins") + - theme(legend.position = "none") - -print(p1) - -#JABS Distance measures -p1 <- data.nextflow|> - select(contains(c("network", "locomotion")))|> - select(contains("network") | (starts_with("bin_sum_")))|> - select(contains("network") | (ends_with("threshold")))|> - pivot_longer( - cols = starts_with("bin_sum_"), - names_to = "bin_size", - values_to = "count" - ) |> - extract( - bin_size, - into = c("bin_size", "behavior"), - regex = "bin_sum_(\\d+)_([\\S]+)_cm_threshold", - convert = TRUE - ) |> - ggplot(aes(x = bin_size, y = count)) + - geom_line(aes(group = network_filename, colour = network_filename)) + - geom_point(aes(group = network_filename, colour = network_filename)) + - stat_summary(geom = "line", fun.data = mean_se) + - stat_summary(geom = "ribbon", alpha = 0.3) + - stat_summary(geom = "point", fun.data = mean_se) + - facet_wrap(facets = vars(behavior)) + - labs(title = "JABS Locomotion", - y = "cm", - x = "mins") + - theme(legend.position = "none") - -print(p1) - -dev.off() - - - - -#Preliminary QC figures: outlines from linear model of 2 phenotypes------------- - -#Publish figures of linear relationships between 2 variables, with the 5 most distant points labeled -#see doc in function section for details -pdf(file.path(qc.figs.dir, "scatter_plot_lm_figs.pdf"), 7,7) -print( - qc_plot_lm_outliers(data.nextflow$distance_traveled, - data.nextflow$bin_avg_55_locomotion_distance_cm, - data.nextflow$network_filename) -) - -print( - qc_plot_lm_outliers(data.nextflow$bin_sum_55_jumping_bout_behavior, - data.nextflow$bin_sum_55_escape_bout_behavior, - data.nextflow$network_filename) -) - -print(qc_plot_lm_outliers(data.nextflow$bin_sum_55_freeze_bout_behavior, - data.nextflow$bin_sum_55_freezing_bout_behavior, - data.nextflow$network_filename) -) - -print(qc_plot_lm_outliers(data.nextflow$distance_traveled, - data.nextflow$speed, - data.nextflow$network_filename) -) - -print(qc_plot_lm_outliers(data.nextflow$bin_sum_55_in_periphery_time_secs, - data.nextflow$bin_sum_55_in_corner_time_secs, - data.nextflow$network_filename) -) -dev.off() - - -#Preliminary QC figures: checking for outliers using z-score----- -#Subset datasets and create Network_filename + phenotypes - - #Gait outlier screening -gait.outliers <- unified_zscore_processing(data.nextflow[colnames(data.nextflow) %in% gait.cols], zscore.threshold) +gait.outliers <- unified_zscore_processing(data.Nextflow[colnames(data.Nextflow) %in% gait.cols], zscore.threshold) if(length(gait.outliers) != 0){ pdf(file.path(zscore.boxplot.dir, "gait_boxplot_outlier_figs.pdf"), 16, 20) @@ -566,7 +463,7 @@ if(length(gait.outliers) != 0){ } #Morphometric outlier screening -morpho.outliers <- unified_zscore_processing(data.nextflow[colnames(data.nextflow) %in% morpho.cols], zscore.threshold) +morpho.outliers <- unified_zscore_processing(data.Nextflow[colnames(data.Nextflow) %in% morpho.cols], zscore.threshold) if(length(morpho.outliers) != 0){ pdf(file.path(zscore.boxplot.dir, "morpho_boxplot_outlier_figs.pdf"), 16, 20) @@ -577,7 +474,10 @@ if(length(morpho.outliers) != 0){ #JABSmetric outlier screening #I should really subset these features -JABS.outliers <- unified_zscore_processing(data.nextflow[colnames(data.nextflow) %in% JABS.cols], zscore.threshold) +JABS.outliers <- unified_zscore_processing(data.Nextflow[colnames(data.Nextflow) %in% JABS.cols], zscore.threshold) + +unique(JABS.outliers$phenotype) + #Subset JABS figures if there are too many cols if(length(JABS.outliers) != 0){ diff --git a/corr_heatmaps_4.R b/r/5_heatmap.R similarity index 91% rename from corr_heatmaps_4.R rename to r/5_heatmap.R index f0f2fec..f3521eb 100644 --- a/corr_heatmaps_4.R +++ b/r/5_heatmap.R @@ -25,7 +25,7 @@ options(error = NULL) #working.directory <- "C:\\Users\\beierj\\Desktop\\2025-04-09_NTG_C1-C5_Analysis\\" ##Metadata for partial and regular correlations -metadata.to.retain <- c("sex", "test_date", "gx") +metadata.to.retain <- c("sex", "day", "tx") #Create functions-------------------------------------------------------------- @@ -70,7 +70,7 @@ rm_df_to_cor_output_list <- function(df, rm, corr_var){ output <- NULL #Create a list of unique values for repeated measure - repeated.measure <- sort(unique(df[[rm]])) + repeated.measure <- sort(unique(df[,rm])) #Ensure the cols are ordered the way you'd like below df <- cbind(df[c(rm, corr_var)], @@ -113,7 +113,11 @@ rm_df_to_cor_output_list <- function(df, rm, corr_var){ } - + output |> + group_by(day) |> + summarize( + count = n() + ) output <- list(r = output[c("measure", "r", paste(rm))], pval = output[c("measure", "pval", paste(rm))] ) @@ -147,13 +151,11 @@ rm_df_to_cor_output_list <- function(df, rm, corr_var){ output } - - rm_df_to_pcor_output_list <- function(df, rm, corr_var, cont_var){ output <- NULL #Create a list of unique values for repeated measure - repeated.measure <- sort(unique(df[[rm]])) + repeated.measure <- sort(unique(df[,rm])) #Ensure the cols are ordered the way you'd like below df <- cbind(df[c(rm, corr_var, cont_var)], @@ -328,20 +330,19 @@ data.nextflow.m <- subset(data.nextflow, sex == 1) |> #----Loop for heatmaps---- -pcorr.all.tx <- rm_df_to_pcor_output_list(data.nextflow, "test_date", "gx", "sex") - +corr.all.tx <- rm_df_to_pcor_output_list(data.nextflow, "day", "tx", "sex") -corr.all.tx <- rm_df_to_cor_output_list(data.nextflow[-2], "test_date", "gx") +corr.all.tx.f <- rm_df_to_cor_output_list(data.nextflow.f, "day", "tx") #Make some heatmaps----- -ht.all.corr.05.noadj <- plot_heatmap(corr.all.tx, 0.05, adjusted = TRUE, - min_sig=1, min_sig_exclude_row_1 = FALSE, col_titles = "Day", - height = 40, width = 1.5) +ht.all.corr.05.noadj <- plot_heatmap(corr.all.tx, 0.05, adjusted = FALSE, + min_sig=2, min_sig_exclude_row_1 = TRUE, col_titles = "Day", + height = 7.5, width = 20) -pdf(file="test3.pdf", width = 7.5, height = 40) +pdf(file="exploratory_figs/Heatmaps/test.pdf", width = 7.5, height = 12.5) #png(file="exploratory_figs/Heatmaps/All_mice_tx_corr_05_pval.png", width = 17.5, height = 30, units= "cm", res = 600) -draw(ht.all.corr.05.noadj, column_title = "NTG:Behavior Correlation, all mice", column_title_gp = gpar(fontsize = 25)) +draw(test, column_title = "NTG:Behavior Correlation, all mice", column_title_gp = gpar(fontsize = 25)) dev.off() diff --git a/renv.lock b/renv.lock new file mode 100644 index 0000000..4e3b4b1 --- /dev/null +++ b/renv.lock @@ -0,0 +1,1956 @@ +{ + "R": { + "Version": "4.3.1", + "Repositories": [ + { + "Name": "CRAN", + "URL": "https://p3m.dev/cran/2023-10-30" + } + ] + }, + "Bioconductor": { + "Version": "3.18" + }, + "Packages": { + "BiocGenerics": { + "Package": "BiocGenerics", + "Version": "0.48.1", + "Source": "Bioconductor", + "Requirements": [ + "R", + "graphics", + "methods", + "stats", + "utils" + ], + "Hash": "e34278c65d7dffcc08f737bf0944ca9a" + }, + "BiocManager": { + "Package": "BiocManager", + "Version": "1.30.22", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "utils" + ], + "Hash": "d57e43105a1aa9cb54fdb4629725acb1" + }, + "BiocVersion": { + "Package": "BiocVersion", + "Version": "3.18.1", + "Source": "Bioconductor", + "Requirements": [ + "R" + ], + "Hash": "2ecaed86684f5fae76ed5530f9d29c4a" + }, + "ComplexHeatmap": { + "Package": "ComplexHeatmap", + "Version": "2.18.0", + "Source": "Bioconductor", + "Requirements": [ + "GetoptLong", + "GlobalOptions", + "IRanges", + "R", + "RColorBrewer", + "circlize", + "clue", + "codetools", + "colorspace", + "digest", + "doParallel", + "foreach", + "grDevices", + "graphics", + "grid", + "matrixStats", + "methods", + "png", + "stats" + ], + "Hash": "fd8d03c43e175afce12c1012711a05cc" + }, + "DBI": { + "Package": "DBI", + "Version": "1.1.3", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "methods" + ], + "Hash": "b2866e62bab9378c3cc9476a1954226b" + }, + "GetoptLong": { + "Package": "GetoptLong", + "Version": "1.0.5", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "GlobalOptions", + "R", + "crayon", + "methods", + "rjson" + ], + "Hash": "61fac01c73abf03ac72e88dc3952c1e3" + }, + "GlobalOptions": { + "Package": "GlobalOptions", + "Version": "0.1.2", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "methods", + "utils" + ], + "Hash": "c3f7b221e60c28f5f3533d74c6fef024" + }, + "IRanges": { + "Package": "IRanges", + "Version": "2.36.0", + "Source": "Bioconductor", + "Requirements": [ + "BiocGenerics", + "R", + "S4Vectors", + "methods", + "stats", + "stats4", + "utils" + ], + "Hash": "f98500eeb93e8a66ad65be955a848595" + }, + "MASS": { + "Package": "MASS", + "Version": "7.3-60", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "grDevices", + "graphics", + "methods", + "stats", + "utils" + ], + "Hash": "a56a6365b3fa73293ea8d084be0d9bb0" + }, + "Matrix": { + "Package": "Matrix", + "Version": "1.6-1.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "grDevices", + "graphics", + "grid", + "lattice", + "methods", + "stats", + "utils" + ], + "Hash": "1a00d4828f33a9d690806e98bd17150c" + }, + "R6": { + "Package": "R6", + "Version": "2.5.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R" + ], + "Hash": "470851b6d5d0ac559e9d01bb352b4021" + }, + "RColorBrewer": { + "Package": "RColorBrewer", + "Version": "1.1-3", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R" + ], + "Hash": "45f0398006e83a5b10b72a90663d8d8c" + }, + "Rcpp": { + "Package": "Rcpp", + "Version": "1.0.11", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "methods", + "utils" + ], + "Hash": "ae6cbbe1492f4de79c45fce06f967ce8" + }, + "S4Vectors": { + "Package": "S4Vectors", + "Version": "0.40.2", + "Source": "Bioconductor", + "Repository": "Bioconductor 3.18", + "Requirements": [ + "BiocGenerics", + "R", + "methods", + "stats", + "stats4", + "utils" + ], + "Hash": "1716e201f81ced0f456dd5ec85fe20f8" + }, + "askpass": { + "Package": "askpass", + "Version": "1.2.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "sys" + ], + "Hash": "cad6cf7f1d5f6e906700b9d3e718c796" + }, + "backports": { + "Package": "backports", + "Version": "1.4.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R" + ], + "Hash": "c39fbec8a30d23e721980b8afb31984c" + }, + "base64enc": { + "Package": "base64enc", + "Version": "0.1-3", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R" + ], + "Hash": "543776ae6848fde2f48ff3816d0628bc" + }, + "bayestestR": { + "Package": "bayestestR", + "Version": "0.13.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "datawizard", + "graphics", + "insight", + "methods", + "stats", + "utils" + ], + "Hash": "61f643ea5ee9fe0e70ab0246340b3c2e" + }, + "bit": { + "Package": "bit", + "Version": "4.0.5", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R" + ], + "Hash": "d242abec29412ce988848d0294b208fd" + }, + "bit64": { + "Package": "bit64", + "Version": "4.0.5", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "bit", + "methods", + "stats", + "utils" + ], + "Hash": "9fe98599ca456d6552421db0d6772d8f" + }, + "blob": { + "Package": "blob", + "Version": "1.2.4", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "methods", + "rlang", + "vctrs" + ], + "Hash": "40415719b5a479b87949f3aa0aee737c" + }, + "broom": { + "Package": "broom", + "Version": "1.0.5", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "backports", + "dplyr", + "ellipsis", + "generics", + "glue", + "lifecycle", + "purrr", + "rlang", + "stringr", + "tibble", + "tidyr" + ], + "Hash": "fd25391c3c4f6ecf0fa95f1e6d15378c" + }, + "bslib": { + "Package": "bslib", + "Version": "0.5.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "base64enc", + "cachem", + "grDevices", + "htmltools", + "jquerylib", + "jsonlite", + "memoise", + "mime", + "rlang", + "sass" + ], + "Hash": "283015ddfbb9d7bf15ea9f0b5698f0d9" + }, + "cachem": { + "Package": "cachem", + "Version": "1.0.8", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "fastmap", + "rlang" + ], + "Hash": "c35768291560ce302c0a6589f92e837d" + }, + "callr": { + "Package": "callr", + "Version": "3.7.3", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "R6", + "processx", + "utils" + ], + "Hash": "9b2191ede20fa29828139b9900922e51" + }, + "cellranger": { + "Package": "cellranger", + "Version": "1.1.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "rematch", + "tibble" + ], + "Hash": "f61dbaec772ccd2e17705c1e872e9e7c" + }, + "circlize": { + "Package": "circlize", + "Version": "0.4.15", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "GlobalOptions", + "R", + "colorspace", + "grDevices", + "graphics", + "grid", + "methods", + "shape", + "stats", + "utils" + ], + "Hash": "2bb47a2fe6ab009b1dcc5566d8c3a988" + }, + "cli": { + "Package": "cli", + "Version": "3.6.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "utils" + ], + "Hash": "89e6d8219950eac806ae0c489052048a" + }, + "clipr": { + "Package": "clipr", + "Version": "0.8.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "utils" + ], + "Hash": "3f038e5ac7f41d4ac41ce658c85e3042" + }, + "clue": { + "Package": "clue", + "Version": "0.3-65", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "cluster", + "graphics", + "methods", + "stats" + ], + "Hash": "d6b53853800595408a776900bcc0c23f" + }, + "cluster": { + "Package": "cluster", + "Version": "2.1.4", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "grDevices", + "graphics", + "stats", + "utils" + ], + "Hash": "5edbbabab6ce0bf7900a74fd4358628e" + }, + "codetools": { + "Package": "codetools", + "Version": "0.2-19", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R" + ], + "Hash": "c089a619a7fae175d149d89164f8c7d8" + }, + "colorRamp2": { + "Package": "colorRamp2", + "Version": "0.1.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "colorspace", + "grDevices", + "methods", + "stats" + ], + "Hash": "9d3ab31de2c98399da370982a23733b6" + }, + "colorspace": { + "Package": "colorspace", + "Version": "2.1-0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "grDevices", + "graphics", + "methods", + "stats" + ], + "Hash": "f20c47fd52fae58b4e377c37bb8c335b" + }, + "conflicted": { + "Package": "conflicted", + "Version": "1.2.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "cli", + "memoise", + "rlang" + ], + "Hash": "bb097fccb22d156624fd07cd2894ddb6" + }, + "correlation": { + "Package": "correlation", + "Version": "0.8.4", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "bayestestR", + "datasets", + "datawizard", + "insight", + "parameters", + "stats" + ], + "Hash": "d8bd29a9abda6eed9aaab3ba5769f231" + }, + "cpp11": { + "Package": "cpp11", + "Version": "0.4.6", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R" + ], + "Hash": "707fae4bbf73697ec8d85f9d7076c061" + }, + "crayon": { + "Package": "crayon", + "Version": "1.5.2", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "grDevices", + "methods", + "utils" + ], + "Hash": "e8a1e41acf02548751f45c718d55aa6a" + }, + "curl": { + "Package": "curl", + "Version": "5.1.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R" + ], + "Hash": "9123f3ef96a2c1a93927d828b2fe7d4c" + }, + "data.table": { + "Package": "data.table", + "Version": "1.14.8", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "methods" + ], + "Hash": "b4c06e554f33344e044ccd7fdca750a9" + }, + "datawizard": { + "Package": "datawizard", + "Version": "0.9.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "insight", + "stats", + "utils" + ], + "Hash": "1706690277f29a2ee69d59483e21c5c6" + }, + "dbplyr": { + "Package": "dbplyr", + "Version": "2.4.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "DBI", + "R", + "R6", + "blob", + "cli", + "dplyr", + "glue", + "lifecycle", + "magrittr", + "methods", + "pillar", + "purrr", + "rlang", + "tibble", + "tidyr", + "tidyselect", + "utils", + "vctrs", + "withr" + ], + "Hash": "59351f28a81f0742720b85363c4fdd61" + }, + "digest": { + "Package": "digest", + "Version": "0.6.33", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "utils" + ], + "Hash": "b18a9cf3c003977b0cc49d5e76ebe48d" + }, + "doParallel": { + "Package": "doParallel", + "Version": "1.0.17", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "foreach", + "iterators", + "parallel", + "utils" + ], + "Hash": "451e5edf411987991ab6a5410c45011f" + }, + "dplyr": { + "Package": "dplyr", + "Version": "1.1.3", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "R6", + "cli", + "generics", + "glue", + "lifecycle", + "magrittr", + "methods", + "pillar", + "rlang", + "tibble", + "tidyselect", + "utils", + "vctrs" + ], + "Hash": "e85ffbebaad5f70e1a2e2ef4302b4949" + }, + "dtplyr": { + "Package": "dtplyr", + "Version": "1.3.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "cli", + "data.table", + "dplyr", + "glue", + "lifecycle", + "rlang", + "tibble", + "tidyselect", + "vctrs" + ], + "Hash": "54ed3ea01b11e81a86544faaecfef8e2" + }, + "ellipsis": { + "Package": "ellipsis", + "Version": "0.3.2", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "rlang" + ], + "Hash": "bb0eec2fe32e88d9e2836c2f73ea2077" + }, + "evaluate": { + "Package": "evaluate", + "Version": "0.22", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "methods" + ], + "Hash": "66f39c7a21e03c4dcb2c2d21d738d603" + }, + "fansi": { + "Package": "fansi", + "Version": "1.0.5", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "grDevices", + "utils" + ], + "Hash": "3e8583a60163b4bc1a80016e63b9959e" + }, + "farver": { + "Package": "farver", + "Version": "2.1.1", + "Source": "Repository", + "Repository": "RSPM", + "Hash": "8106d78941f34855c440ddb946b8f7a5" + }, + "fastmap": { + "Package": "fastmap", + "Version": "1.1.1", + "Source": "Repository", + "Repository": "RSPM", + "Hash": "f7736a18de97dea803bde0a2daaafb27" + }, + "fontawesome": { + "Package": "fontawesome", + "Version": "0.5.2", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "htmltools", + "rlang" + ], + "Hash": "c2efdd5f0bcd1ea861c2d4e2a883a67d" + }, + "forcats": { + "Package": "forcats", + "Version": "1.0.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "cli", + "glue", + "lifecycle", + "magrittr", + "rlang", + "tibble" + ], + "Hash": "1a0a9a3d5083d0d573c4214576f1e690" + }, + "foreach": { + "Package": "foreach", + "Version": "1.5.2", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "codetools", + "iterators", + "utils" + ], + "Hash": "618609b42c9406731ead03adf5379850" + }, + "fs": { + "Package": "fs", + "Version": "1.6.3", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "methods" + ], + "Hash": "47b5f30c720c23999b913a1a635cf0bb" + }, + "gargle": { + "Package": "gargle", + "Version": "1.5.2", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "cli", + "fs", + "glue", + "httr", + "jsonlite", + "lifecycle", + "openssl", + "rappdirs", + "rlang", + "stats", + "utils", + "withr" + ], + "Hash": "fc0b272e5847c58cd5da9b20eedbd026" + }, + "generics": { + "Package": "generics", + "Version": "0.1.3", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "methods" + ], + "Hash": "15e9634c0fcd294799e9b2e929ed1b86" + }, + "getopt": { + "Package": "getopt", + "Version": "1.20.4", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "stats" + ], + "Hash": "ed33b16c6d24f7ced1d68877ac2509ee" + }, + "ggplot2": { + "Package": "ggplot2", + "Version": "3.4.4", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "MASS", + "R", + "cli", + "glue", + "grDevices", + "grid", + "gtable", + "isoband", + "lifecycle", + "mgcv", + "rlang", + "scales", + "stats", + "tibble", + "vctrs", + "withr" + ], + "Hash": "313d31eff2274ecf4c1d3581db7241f9" + }, + "ggrepel": { + "Package": "ggrepel", + "Version": "0.9.4", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "Rcpp", + "ggplot2", + "grid", + "rlang", + "scales", + "withr" + ], + "Hash": "e9839af82cc43fda486a638b68b439b2" + }, + "glue": { + "Package": "glue", + "Version": "1.6.2", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "methods" + ], + "Hash": "4f2596dfb05dac67b9dc558e5c6fba2e" + }, + "googledrive": { + "Package": "googledrive", + "Version": "2.1.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "cli", + "gargle", + "glue", + "httr", + "jsonlite", + "lifecycle", + "magrittr", + "pillar", + "purrr", + "rlang", + "tibble", + "utils", + "uuid", + "vctrs", + "withr" + ], + "Hash": "e99641edef03e2a5e87f0a0b1fcc97f4" + }, + "googlesheets4": { + "Package": "googlesheets4", + "Version": "1.1.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "cellranger", + "cli", + "curl", + "gargle", + "glue", + "googledrive", + "httr", + "ids", + "lifecycle", + "magrittr", + "methods", + "purrr", + "rematch2", + "rlang", + "tibble", + "utils", + "vctrs", + "withr" + ], + "Hash": "d6db1667059d027da730decdc214b959" + }, + "gtable": { + "Package": "gtable", + "Version": "0.3.4", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "cli", + "glue", + "grid", + "lifecycle", + "rlang" + ], + "Hash": "b29cf3031f49b04ab9c852c912547eef" + }, + "haven": { + "Package": "haven", + "Version": "2.5.3", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "cli", + "cpp11", + "forcats", + "hms", + "lifecycle", + "methods", + "readr", + "rlang", + "tibble", + "tidyselect", + "vctrs" + ], + "Hash": "9b302fe352f9cfc5dcf0a4139af3a565" + }, + "highr": { + "Package": "highr", + "Version": "0.10", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "xfun" + ], + "Hash": "06230136b2d2b9ba5805e1963fa6e890" + }, + "hms": { + "Package": "hms", + "Version": "1.1.3", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "lifecycle", + "methods", + "pkgconfig", + "rlang", + "vctrs" + ], + "Hash": "b59377caa7ed00fa41808342002138f9" + }, + "htmltools": { + "Package": "htmltools", + "Version": "0.5.6.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "base64enc", + "digest", + "ellipsis", + "fastmap", + "grDevices", + "rlang", + "utils" + ], + "Hash": "1e12fe667316a76508898839ecfb2d00" + }, + "httr": { + "Package": "httr", + "Version": "1.4.7", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "R6", + "curl", + "jsonlite", + "mime", + "openssl" + ], + "Hash": "ac107251d9d9fd72f0ca8049988f1d7f" + }, + "ids": { + "Package": "ids", + "Version": "1.0.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "openssl", + "uuid" + ], + "Hash": "99df65cfef20e525ed38c3d2577f7190" + }, + "insight": { + "Package": "insight", + "Version": "0.19.6", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "methods", + "stats", + "utils" + ], + "Hash": "cc21c0957774c1602ec3324a5a47d798" + }, + "isoband": { + "Package": "isoband", + "Version": "0.2.7", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "grid", + "utils" + ], + "Hash": "0080607b4a1a7b28979aecef976d8bc2" + }, + "iterators": { + "Package": "iterators", + "Version": "1.0.14", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "utils" + ], + "Hash": "8954069286b4b2b0d023d1b288dce978" + }, + "janitor": { + "Package": "janitor", + "Version": "2.2.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "dplyr", + "hms", + "lifecycle", + "lubridate", + "magrittr", + "purrr", + "rlang", + "snakecase", + "stringi", + "stringr", + "tidyr", + "tidyselect" + ], + "Hash": "5baae149f1082f466df9d1442ba7aa65" + }, + "jquerylib": { + "Package": "jquerylib", + "Version": "0.1.4", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "htmltools" + ], + "Hash": "5aab57a3bd297eee1c1d862735972182" + }, + "jsonlite": { + "Package": "jsonlite", + "Version": "1.8.7", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "methods" + ], + "Hash": "266a20443ca13c65688b2116d5220f76" + }, + "knitr": { + "Package": "knitr", + "Version": "1.44", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "evaluate", + "highr", + "methods", + "tools", + "xfun", + "yaml" + ], + "Hash": "60885b9f746c9dfaef110d070b5f7dc0" + }, + "labeling": { + "Package": "labeling", + "Version": "0.4.3", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "graphics", + "stats" + ], + "Hash": "b64ec208ac5bc1852b285f665d6368b3" + }, + "lattice": { + "Package": "lattice", + "Version": "0.21-8", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "grDevices", + "graphics", + "grid", + "stats", + "utils" + ], + "Hash": "0b8a6d63c8770f02a8b5635f3c431e6b" + }, + "lifecycle": { + "Package": "lifecycle", + "Version": "1.0.3", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "cli", + "glue", + "rlang" + ], + "Hash": "001cecbeac1cff9301bdc3775ee46a86" + }, + "lubridate": { + "Package": "lubridate", + "Version": "1.9.3", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "generics", + "methods", + "timechange" + ], + "Hash": "680ad542fbcf801442c83a6ac5a2126c" + }, + "magrittr": { + "Package": "magrittr", + "Version": "2.0.3", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R" + ], + "Hash": "7ce2733a9826b3aeb1775d56fd305472" + }, + "matrixStats": { + "Package": "matrixStats", + "Version": "1.0.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R" + ], + "Hash": "9143629fd64335aac6a6250d1c1ed82a" + }, + "memoise": { + "Package": "memoise", + "Version": "2.0.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "cachem", + "rlang" + ], + "Hash": "e2817ccf4a065c5d9d7f2cfbe7c1d78c" + }, + "mgcv": { + "Package": "mgcv", + "Version": "1.8-42", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "Matrix", + "R", + "graphics", + "methods", + "nlme", + "splines", + "stats", + "utils" + ], + "Hash": "3460beba7ccc8946249ba35327ba902a" + }, + "mime": { + "Package": "mime", + "Version": "0.12", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "tools" + ], + "Hash": "18e9c28c1d3ca1560ce30658b22ce104" + }, + "modelr": { + "Package": "modelr", + "Version": "0.1.11", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "broom", + "magrittr", + "purrr", + "rlang", + "tibble", + "tidyr", + "tidyselect", + "vctrs" + ], + "Hash": "4f50122dc256b1b6996a4703fecea821" + }, + "munsell": { + "Package": "munsell", + "Version": "0.5.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "colorspace", + "methods" + ], + "Hash": "6dfe8bf774944bd5595785e3229d8771" + }, + "nlme": { + "Package": "nlme", + "Version": "3.1-162", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "graphics", + "lattice", + "stats", + "utils" + ], + "Hash": "0984ce8da8da9ead8643c5cbbb60f83e" + }, + "openssl": { + "Package": "openssl", + "Version": "2.1.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "askpass" + ], + "Hash": "2a0dc8c6adfb6f032e4d4af82d258ab5" + }, + "optparse": { + "Package": "optparse", + "Version": "1.7.3", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "getopt", + "methods" + ], + "Hash": "aa4a7717b5760a769c7fd3d34614f2a2" + }, + "parameters": { + "Package": "parameters", + "Version": "0.21.2", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "bayestestR", + "datawizard", + "graphics", + "insight", + "methods", + "stats", + "utils" + ], + "Hash": "7bca0c1c6f188b195a5f380b8e73b91a" + }, + "pillar": { + "Package": "pillar", + "Version": "1.9.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "cli", + "fansi", + "glue", + "lifecycle", + "rlang", + "utf8", + "utils", + "vctrs" + ], + "Hash": "15da5a8412f317beeee6175fbc76f4bb" + }, + "pkgconfig": { + "Package": "pkgconfig", + "Version": "2.0.3", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "utils" + ], + "Hash": "01f28d4278f15c76cddbea05899c5d6f" + }, + "png": { + "Package": "png", + "Version": "0.1-8", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R" + ], + "Hash": "bd54ba8a0a5faded999a7aab6e46b374" + }, + "ppcor": { + "Package": "ppcor", + "Version": "1.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "MASS", + "R" + ], + "Hash": "0b26c0c84f22515249dd7915f4214d32" + }, + "prettyunits": { + "Package": "prettyunits", + "Version": "1.2.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R" + ], + "Hash": "6b01fc98b1e86c4f705ce9dcfd2f57c7" + }, + "processx": { + "Package": "processx", + "Version": "3.8.2", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "R6", + "ps", + "utils" + ], + "Hash": "3efbd8ac1be0296a46c55387aeace0f3" + }, + "progress": { + "Package": "progress", + "Version": "1.2.2", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R6", + "crayon", + "hms", + "prettyunits" + ], + "Hash": "14dc9f7a3c91ebb14ec5bb9208a07061" + }, + "ps": { + "Package": "ps", + "Version": "1.7.5", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "utils" + ], + "Hash": "709d852d33178db54b17c722e5b1e594" + }, + "purrr": { + "Package": "purrr", + "Version": "1.0.2", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "cli", + "lifecycle", + "magrittr", + "rlang", + "vctrs" + ], + "Hash": "1cba04a4e9414bdefc9dcaa99649a8dc" + }, + "ragg": { + "Package": "ragg", + "Version": "1.2.6", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "systemfonts", + "textshaping" + ], + "Hash": "6ba2fa8740abdc2cc148407836509901" + }, + "rappdirs": { + "Package": "rappdirs", + "Version": "0.3.3", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R" + ], + "Hash": "5e3c5dc0b071b21fa128676560dbe94d" + }, + "readr": { + "Package": "readr", + "Version": "2.1.4", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "R6", + "cli", + "clipr", + "cpp11", + "crayon", + "hms", + "lifecycle", + "methods", + "rlang", + "tibble", + "tzdb", + "utils", + "vroom" + ], + "Hash": "b5047343b3825f37ad9d3b5d89aa1078" + }, + "readxl": { + "Package": "readxl", + "Version": "1.4.3", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "cellranger", + "cpp11", + "progress", + "tibble", + "utils" + ], + "Hash": "8cf9c239b96df1bbb133b74aef77ad0a" + }, + "rematch": { + "Package": "rematch", + "Version": "2.0.0", + "Source": "Repository", + "Repository": "RSPM", + "Hash": "cbff1b666c6fa6d21202f07e2318d4f1" + }, + "rematch2": { + "Package": "rematch2", + "Version": "2.1.2", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "tibble" + ], + "Hash": "76c9e04c712a05848ae7a23d2f170a40" + }, + "renv": { + "Package": "renv", + "Version": "1.0.3", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "utils" + ], + "Hash": "41b847654f567341725473431dd0d5ab" + }, + "reprex": { + "Package": "reprex", + "Version": "2.0.2", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "callr", + "cli", + "clipr", + "fs", + "glue", + "knitr", + "lifecycle", + "rlang", + "rmarkdown", + "rstudioapi", + "utils", + "withr" + ], + "Hash": "d66fe009d4c20b7ab1927eb405db9ee2" + }, + "rjson": { + "Package": "rjson", + "Version": "0.2.21", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R" + ], + "Hash": "f9da75e6444e95a1baf8ca24909d63b9" + }, + "rlang": { + "Package": "rlang", + "Version": "1.1.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "utils" + ], + "Hash": "a85c767b55f0bf9b7ad16c6d7baee5bb" + }, + "rmarkdown": { + "Package": "rmarkdown", + "Version": "2.25", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "bslib", + "evaluate", + "fontawesome", + "htmltools", + "jquerylib", + "jsonlite", + "knitr", + "methods", + "stringr", + "tinytex", + "tools", + "utils", + "xfun", + "yaml" + ], + "Hash": "d65e35823c817f09f4de424fcdfa812a" + }, + "rstudioapi": { + "Package": "rstudioapi", + "Version": "0.15.0", + "Source": "Repository", + "Repository": "RSPM", + "Hash": "5564500e25cffad9e22244ced1379887" + }, + "rvest": { + "Package": "rvest", + "Version": "1.0.3", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "cli", + "glue", + "httr", + "lifecycle", + "magrittr", + "rlang", + "selectr", + "tibble", + "withr", + "xml2" + ], + "Hash": "a4a5ac819a467808c60e36e92ddf195e" + }, + "sass": { + "Package": "sass", + "Version": "0.4.7", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R6", + "fs", + "htmltools", + "rappdirs", + "rlang" + ], + "Hash": "6bd4d33b50ff927191ec9acbf52fd056" + }, + "scales": { + "Package": "scales", + "Version": "1.2.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "R6", + "RColorBrewer", + "farver", + "labeling", + "lifecycle", + "munsell", + "rlang", + "viridisLite" + ], + "Hash": "906cb23d2f1c5680b8ce439b44c6fa63" + }, + "selectr": { + "Package": "selectr", + "Version": "0.4-2", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "R6", + "methods", + "stringr" + ], + "Hash": "3838071b66e0c566d55cc26bd6e27bf4" + }, + "shape": { + "Package": "shape", + "Version": "1.4.6", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "grDevices", + "graphics", + "stats" + ], + "Hash": "9067f962730f58b14d8ae54ca885509f" + }, + "snakecase": { + "Package": "snakecase", + "Version": "0.11.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "stringi", + "stringr" + ], + "Hash": "58767e44739b76965332e8a4fe3f91f1" + }, + "stringi": { + "Package": "stringi", + "Version": "1.7.12", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "stats", + "tools", + "utils" + ], + "Hash": "ca8bd84263c77310739d2cf64d84d7c9" + }, + "stringr": { + "Package": "stringr", + "Version": "1.5.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "cli", + "glue", + "lifecycle", + "magrittr", + "rlang", + "stringi", + "vctrs" + ], + "Hash": "671a4d384ae9d32fc47a14e98bfa3dc8" + }, + "sys": { + "Package": "sys", + "Version": "3.4.2", + "Source": "Repository", + "Repository": "RSPM", + "Hash": "3a1be13d68d47a8cd0bfd74739ca1555" + }, + "systemfonts": { + "Package": "systemfonts", + "Version": "1.0.5", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "cpp11" + ], + "Hash": "15b594369e70b975ba9f064295983499" + }, + "textshaping": { + "Package": "textshaping", + "Version": "0.3.7", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "cpp11", + "systemfonts" + ], + "Hash": "997aac9ad649e0ef3b97f96cddd5622b" + }, + "tibble": { + "Package": "tibble", + "Version": "3.2.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "fansi", + "lifecycle", + "magrittr", + "methods", + "pillar", + "pkgconfig", + "rlang", + "utils", + "vctrs" + ], + "Hash": "a84e2cc86d07289b3b6f5069df7a004c" + }, + "tidyr": { + "Package": "tidyr", + "Version": "1.3.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "cli", + "cpp11", + "dplyr", + "glue", + "lifecycle", + "magrittr", + "purrr", + "rlang", + "stringr", + "tibble", + "tidyselect", + "utils", + "vctrs" + ], + "Hash": "e47debdc7ce599b070c8e78e8ac0cfcf" + }, + "tidyselect": { + "Package": "tidyselect", + "Version": "1.2.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "cli", + "glue", + "lifecycle", + "rlang", + "vctrs", + "withr" + ], + "Hash": "79540e5fcd9e0435af547d885f184fd5" + }, + "tidyverse": { + "Package": "tidyverse", + "Version": "2.0.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "broom", + "cli", + "conflicted", + "dbplyr", + "dplyr", + "dtplyr", + "forcats", + "ggplot2", + "googledrive", + "googlesheets4", + "haven", + "hms", + "httr", + "jsonlite", + "lubridate", + "magrittr", + "modelr", + "pillar", + "purrr", + "ragg", + "readr", + "readxl", + "reprex", + "rlang", + "rstudioapi", + "rvest", + "stringr", + "tibble", + "tidyr", + "xml2" + ], + "Hash": "c328568cd14ea89a83bd4ca7f54ae07e" + }, + "timechange": { + "Package": "timechange", + "Version": "0.2.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "cpp11" + ], + "Hash": "8548b44f79a35ba1791308b61e6012d7" + }, + "tinytex": { + "Package": "tinytex", + "Version": "0.48", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "xfun" + ], + "Hash": "8f96d229b7311beb32b94cf413b13f84" + }, + "tzdb": { + "Package": "tzdb", + "Version": "0.4.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "cpp11" + ], + "Hash": "f561504ec2897f4d46f0c7657e488ae1" + }, + "utf8": { + "Package": "utf8", + "Version": "1.2.4", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R" + ], + "Hash": "62b65c52671e6665f803ff02954446e9" + }, + "uuid": { + "Package": "uuid", + "Version": "1.1-1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R" + ], + "Hash": "3d78edfb977a69fc7a0341bee25e163f" + }, + "vctrs": { + "Package": "vctrs", + "Version": "0.6.4", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "cli", + "glue", + "lifecycle", + "rlang" + ], + "Hash": "266c1ca411266ba8f365fcc726444b87" + }, + "viridisLite": { + "Package": "viridisLite", + "Version": "0.4.2", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R" + ], + "Hash": "c826c7c4241b6fc89ff55aaea3fa7491" + }, + "vroom": { + "Package": "vroom", + "Version": "1.6.4", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "bit64", + "cli", + "cpp11", + "crayon", + "glue", + "hms", + "lifecycle", + "methods", + "progress", + "rlang", + "stats", + "tibble", + "tidyselect", + "tzdb", + "vctrs", + "withr" + ], + "Hash": "9db52c1656cf19c124f93124ea57f0fd" + }, + "withr": { + "Package": "withr", + "Version": "2.5.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "grDevices", + "graphics", + "stats" + ], + "Hash": "d77c6f74be05c33164e33fbc85540cae" + }, + "writexl": { + "Package": "writexl", + "Version": "1.4.2", + "Source": "Repository", + "Repository": "RSPM", + "Hash": "43da939eaf6681c88eba977b9012dad9" + }, + "xfun": { + "Package": "xfun", + "Version": "0.40", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "stats", + "tools" + ], + "Hash": "be07d23211245fc7d4209f54c4e4ffc8" + }, + "xml2": { + "Package": "xml2", + "Version": "1.3.5", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "methods" + ], + "Hash": "6c40e5cfcc6aefd88110666e18c31f40" + }, + "yaml": { + "Package": "yaml", + "Version": "2.3.7", + "Source": "Repository", + "Repository": "RSPM", + "Hash": "0d0056cc5383fbc240ccd0cb584bf436" + } + } +} diff --git a/renv/.gitignore b/renv/.gitignore new file mode 100644 index 0000000..0ec0cbb --- /dev/null +++ b/renv/.gitignore @@ -0,0 +1,7 @@ +library/ +local/ +cellar/ +lock/ +python/ +sandbox/ +staging/ diff --git a/renv/activate.R b/renv/activate.R new file mode 100644 index 0000000..cb5401f --- /dev/null +++ b/renv/activate.R @@ -0,0 +1,1180 @@ + +local({ + + # the requested version of renv + version <- "1.0.3" + attr(version, "sha") <- NULL + + # the project directory + project <- getwd() + + # use start-up diagnostics if enabled + diagnostics <- Sys.getenv("RENV_STARTUP_DIAGNOSTICS", unset = "FALSE") + if (diagnostics) { + start <- Sys.time() + profile <- tempfile("renv-startup-", fileext = ".Rprof") + utils::Rprof(profile) + on.exit({ + utils::Rprof(NULL) + elapsed <- signif(difftime(Sys.time(), start, units = "auto"), digits = 2L) + writeLines(sprintf("- renv took %s to run the autoloader.", format(elapsed))) + writeLines(sprintf("- Profile: %s", profile)) + print(utils::summaryRprof(profile)) + }, add = TRUE) + } + + # figure out whether the autoloader is enabled + enabled <- local({ + + # first, check config option + override <- getOption("renv.config.autoloader.enabled") + if (!is.null(override)) + return(override) + + # next, check environment variables + # TODO: prefer using the configuration one in the future + envvars <- c( + "RENV_CONFIG_AUTOLOADER_ENABLED", + "RENV_AUTOLOADER_ENABLED", + "RENV_ACTIVATE_PROJECT" + ) + + for (envvar in envvars) { + envval <- Sys.getenv(envvar, unset = NA) + if (!is.na(envval)) + return(tolower(envval) %in% c("true", "t", "1")) + } + + # enable by default + TRUE + + }) + + if (!enabled) + return(FALSE) + + # avoid recursion + if (identical(getOption("renv.autoloader.running"), TRUE)) { + warning("ignoring recursive attempt to run renv autoloader") + return(invisible(TRUE)) + } + + # signal that we're loading renv during R startup + options(renv.autoloader.running = TRUE) + on.exit(options(renv.autoloader.running = NULL), add = TRUE) + + # signal that we've consented to use renv + options(renv.consent = TRUE) + + # load the 'utils' package eagerly -- this ensures that renv shims, which + # mask 'utils' packages, will come first on the search path + library(utils, lib.loc = .Library) + + # unload renv if it's already been loaded + if ("renv" %in% loadedNamespaces()) + unloadNamespace("renv") + + # load bootstrap tools + `%||%` <- function(x, y) { + if (is.null(x)) y else x + } + + catf <- function(fmt, ..., appendLF = TRUE) { + + quiet <- getOption("renv.bootstrap.quiet", default = FALSE) + if (quiet) + return(invisible()) + + msg <- sprintf(fmt, ...) + cat(msg, file = stdout(), sep = if (appendLF) "\n" else "") + + invisible(msg) + + } + + header <- function(label, + ..., + prefix = "#", + suffix = "-", + n = min(getOption("width"), 78)) + { + label <- sprintf(label, ...) + n <- max(n - nchar(label) - nchar(prefix) - 2L, 8L) + if (n <= 0) + return(paste(prefix, label)) + + tail <- paste(rep.int(suffix, n), collapse = "") + paste0(prefix, " ", label, " ", tail) + + } + + startswith <- function(string, prefix) { + substring(string, 1, nchar(prefix)) == prefix + } + + bootstrap <- function(version, library) { + + friendly <- renv_bootstrap_version_friendly(version) + section <- header(sprintf("Bootstrapping renv %s", friendly)) + catf(section) + + # attempt to download renv + catf("- Downloading renv ... ", appendLF = FALSE) + withCallingHandlers( + tarball <- renv_bootstrap_download(version), + error = function(err) { + catf("FAILED") + stop("failed to download:\n", conditionMessage(err)) + } + ) + catf("OK") + on.exit(unlink(tarball), add = TRUE) + + # now attempt to install + catf("- Installing renv ... ", appendLF = FALSE) + withCallingHandlers( + status <- renv_bootstrap_install(version, tarball, library), + error = function(err) { + catf("FAILED") + stop("failed to install:\n", conditionMessage(err)) + } + ) + catf("OK") + + # add empty line to break up bootstrapping from normal output + catf("") + + return(invisible()) + } + + renv_bootstrap_tests_running <- function() { + getOption("renv.tests.running", default = FALSE) + } + + renv_bootstrap_repos <- function() { + + # get CRAN repository + cran <- getOption("renv.repos.cran", "https://cloud.r-project.org") + + # check for repos override + repos <- Sys.getenv("RENV_CONFIG_REPOS_OVERRIDE", unset = NA) + if (!is.na(repos)) { + + # check for RSPM; if set, use a fallback repository for renv + rspm <- Sys.getenv("RSPM", unset = NA) + if (identical(rspm, repos)) + repos <- c(RSPM = rspm, CRAN = cran) + + return(repos) + + } + + # check for lockfile repositories + repos <- tryCatch(renv_bootstrap_repos_lockfile(), error = identity) + if (!inherits(repos, "error") && length(repos)) + return(repos) + + # retrieve current repos + repos <- getOption("repos") + + # ensure @CRAN@ entries are resolved + repos[repos == "@CRAN@"] <- cran + + # add in renv.bootstrap.repos if set + default <- c(FALLBACK = "https://cloud.r-project.org") + extra <- getOption("renv.bootstrap.repos", default = default) + repos <- c(repos, extra) + + # remove duplicates that might've snuck in + dupes <- duplicated(repos) | duplicated(names(repos)) + repos[!dupes] + + } + + renv_bootstrap_repos_lockfile <- function() { + + lockpath <- Sys.getenv("RENV_PATHS_LOCKFILE", unset = "renv.lock") + if (!file.exists(lockpath)) + return(NULL) + + lockfile <- tryCatch(renv_json_read(lockpath), error = identity) + if (inherits(lockfile, "error")) { + warning(lockfile) + return(NULL) + } + + repos <- lockfile$R$Repositories + if (length(repos) == 0) + return(NULL) + + keys <- vapply(repos, `[[`, "Name", FUN.VALUE = character(1)) + vals <- vapply(repos, `[[`, "URL", FUN.VALUE = character(1)) + names(vals) <- keys + + return(vals) + + } + + renv_bootstrap_download <- function(version) { + + sha <- attr(version, "sha", exact = TRUE) + + methods <- if (!is.null(sha)) { + + # attempting to bootstrap a development version of renv + c( + function() renv_bootstrap_download_tarball(sha), + function() renv_bootstrap_download_github(sha) + ) + + } else { + + # attempting to bootstrap a release version of renv + c( + function() renv_bootstrap_download_tarball(version), + function() renv_bootstrap_download_cran_latest(version), + function() renv_bootstrap_download_cran_archive(version) + ) + + } + + for (method in methods) { + path <- tryCatch(method(), error = identity) + if (is.character(path) && file.exists(path)) + return(path) + } + + stop("All download methods failed") + + } + + renv_bootstrap_download_impl <- function(url, destfile) { + + mode <- "wb" + + # https://bugs.r-project.org/bugzilla/show_bug.cgi?id=17715 + fixup <- + Sys.info()[["sysname"]] == "Windows" && + substring(url, 1L, 5L) == "file:" + + if (fixup) + mode <- "w+b" + + args <- list( + url = url, + destfile = destfile, + mode = mode, + quiet = TRUE + ) + + if ("headers" %in% names(formals(utils::download.file))) + args$headers <- renv_bootstrap_download_custom_headers(url) + + do.call(utils::download.file, args) + + } + + renv_bootstrap_download_custom_headers <- function(url) { + + headers <- getOption("renv.download.headers") + if (is.null(headers)) + return(character()) + + if (!is.function(headers)) + stopf("'renv.download.headers' is not a function") + + headers <- headers(url) + if (length(headers) == 0L) + return(character()) + + if (is.list(headers)) + headers <- unlist(headers, recursive = FALSE, use.names = TRUE) + + ok <- + is.character(headers) && + is.character(names(headers)) && + all(nzchar(names(headers))) + + if (!ok) + stop("invocation of 'renv.download.headers' did not return a named character vector") + + headers + + } + + renv_bootstrap_download_cran_latest <- function(version) { + + spec <- renv_bootstrap_download_cran_latest_find(version) + type <- spec$type + repos <- spec$repos + + baseurl <- utils::contrib.url(repos = repos, type = type) + ext <- if (identical(type, "source")) + ".tar.gz" + else if (Sys.info()[["sysname"]] == "Windows") + ".zip" + else + ".tgz" + name <- sprintf("renv_%s%s", version, ext) + url <- paste(baseurl, name, sep = "/") + + destfile <- file.path(tempdir(), name) + status <- tryCatch( + renv_bootstrap_download_impl(url, destfile), + condition = identity + ) + + if (inherits(status, "condition")) + return(FALSE) + + # report success and return + destfile + + } + + renv_bootstrap_download_cran_latest_find <- function(version) { + + # check whether binaries are supported on this system + binary <- + getOption("renv.bootstrap.binary", default = TRUE) && + !identical(.Platform$pkgType, "source") && + !identical(getOption("pkgType"), "source") && + Sys.info()[["sysname"]] %in% c("Darwin", "Windows") + + types <- c(if (binary) "binary", "source") + + # iterate over types + repositories + for (type in types) { + for (repos in renv_bootstrap_repos()) { + + # retrieve package database + db <- tryCatch( + as.data.frame( + utils::available.packages(type = type, repos = repos), + stringsAsFactors = FALSE + ), + error = identity + ) + + if (inherits(db, "error")) + next + + # check for compatible entry + entry <- db[db$Package %in% "renv" & db$Version %in% version, ] + if (nrow(entry) == 0) + next + + # found it; return spec to caller + spec <- list(entry = entry, type = type, repos = repos) + return(spec) + + } + } + + # if we got here, we failed to find renv + fmt <- "renv %s is not available from your declared package repositories" + stop(sprintf(fmt, version)) + + } + + renv_bootstrap_download_cran_archive <- function(version) { + + name <- sprintf("renv_%s.tar.gz", version) + repos <- renv_bootstrap_repos() + urls <- file.path(repos, "src/contrib/Archive/renv", name) + destfile <- file.path(tempdir(), name) + + for (url in urls) { + + status <- tryCatch( + renv_bootstrap_download_impl(url, destfile), + condition = identity + ) + + if (identical(status, 0L)) + return(destfile) + + } + + return(FALSE) + + } + + renv_bootstrap_download_tarball <- function(version) { + + # if the user has provided the path to a tarball via + # an environment variable, then use it + tarball <- Sys.getenv("RENV_BOOTSTRAP_TARBALL", unset = NA) + if (is.na(tarball)) + return() + + # allow directories + if (dir.exists(tarball)) { + name <- sprintf("renv_%s.tar.gz", version) + tarball <- file.path(tarball, name) + } + + # bail if it doesn't exist + if (!file.exists(tarball)) { + + # let the user know we weren't able to honour their request + fmt <- "- RENV_BOOTSTRAP_TARBALL is set (%s) but does not exist." + msg <- sprintf(fmt, tarball) + warning(msg) + + # bail + return() + + } + + catf("- Using local tarball '%s'.", tarball) + tarball + + } + + renv_bootstrap_download_github <- function(version) { + + enabled <- Sys.getenv("RENV_BOOTSTRAP_FROM_GITHUB", unset = "TRUE") + if (!identical(enabled, "TRUE")) + return(FALSE) + + # prepare download options + pat <- Sys.getenv("GITHUB_PAT") + if (nzchar(Sys.which("curl")) && nzchar(pat)) { + fmt <- "--location --fail --header \"Authorization: token %s\"" + extra <- sprintf(fmt, pat) + saved <- options("download.file.method", "download.file.extra") + options(download.file.method = "curl", download.file.extra = extra) + on.exit(do.call(base::options, saved), add = TRUE) + } else if (nzchar(Sys.which("wget")) && nzchar(pat)) { + fmt <- "--header=\"Authorization: token %s\"" + extra <- sprintf(fmt, pat) + saved <- options("download.file.method", "download.file.extra") + options(download.file.method = "wget", download.file.extra = extra) + on.exit(do.call(base::options, saved), add = TRUE) + } + + url <- file.path("https://api.github.com/repos/rstudio/renv/tarball", version) + name <- sprintf("renv_%s.tar.gz", version) + destfile <- file.path(tempdir(), name) + + status <- tryCatch( + renv_bootstrap_download_impl(url, destfile), + condition = identity + ) + + if (!identical(status, 0L)) + return(FALSE) + + renv_bootstrap_download_augment(destfile) + + return(destfile) + + } + + # Add Sha to DESCRIPTION. This is stop gap until #890, after which we + # can use renv::install() to fully capture metadata. + renv_bootstrap_download_augment <- function(destfile) { + sha <- renv_bootstrap_git_extract_sha1_tar(destfile) + if (is.null(sha)) { + return() + } + + # Untar + tempdir <- tempfile("renv-github-") + on.exit(unlink(tempdir, recursive = TRUE), add = TRUE) + untar(destfile, exdir = tempdir) + pkgdir <- dir(tempdir, full.names = TRUE)[[1]] + + # Modify description + desc_path <- file.path(pkgdir, "DESCRIPTION") + desc_lines <- readLines(desc_path) + remotes_fields <- c( + "RemoteType: github", + "RemoteHost: api.github.com", + "RemoteRepo: renv", + "RemoteUsername: rstudio", + "RemotePkgRef: rstudio/renv", + paste("RemoteRef: ", sha), + paste("RemoteSha: ", sha) + ) + writeLines(c(desc_lines[desc_lines != ""], remotes_fields), con = desc_path) + + # Re-tar + local({ + old <- setwd(tempdir) + on.exit(setwd(old), add = TRUE) + + tar(destfile, compression = "gzip") + }) + invisible() + } + + # Extract the commit hash from a git archive. Git archives include the SHA1 + # hash as the comment field of the tarball pax extended header + # (see https://www.kernel.org/pub/software/scm/git/docs/git-archive.html) + # For GitHub archives this should be the first header after the default one + # (512 byte) header. + renv_bootstrap_git_extract_sha1_tar <- function(bundle) { + + # open the bundle for reading + # We use gzcon for everything because (from ?gzcon) + # > Reading from a connection which does not supply a 'gzip' magic + # > header is equivalent to reading from the original connection + conn <- gzcon(file(bundle, open = "rb", raw = TRUE)) + on.exit(close(conn)) + + # The default pax header is 512 bytes long and the first pax extended header + # with the comment should be 51 bytes long + # `52 comment=` (11 chars) + 40 byte SHA1 hash + len <- 0x200 + 0x33 + res <- rawToChar(readBin(conn, "raw", n = len)[0x201:len]) + + if (grepl("^52 comment=", res)) { + sub("52 comment=", "", res) + } else { + NULL + } + } + + renv_bootstrap_install <- function(version, tarball, library) { + + # attempt to install it into project library + dir.create(library, showWarnings = FALSE, recursive = TRUE) + output <- renv_bootstrap_install_impl(library, tarball) + + # check for successful install + status <- attr(output, "status") + if (is.null(status) || identical(status, 0L)) + return(status) + + # an error occurred; report it + header <- "installation of renv failed" + lines <- paste(rep.int("=", nchar(header)), collapse = "") + text <- paste(c(header, lines, output), collapse = "\n") + stop(text) + + } + + renv_bootstrap_install_impl <- function(library, tarball) { + + # invoke using system2 so we can capture and report output + bin <- R.home("bin") + exe <- if (Sys.info()[["sysname"]] == "Windows") "R.exe" else "R" + R <- file.path(bin, exe) + + args <- c( + "--vanilla", "CMD", "INSTALL", "--no-multiarch", + "-l", shQuote(path.expand(library)), + shQuote(path.expand(tarball)) + ) + + system2(R, args, stdout = TRUE, stderr = TRUE) + + } + + renv_bootstrap_platform_prefix <- function() { + + # construct version prefix + version <- paste(R.version$major, R.version$minor, sep = ".") + prefix <- paste("R", numeric_version(version)[1, 1:2], sep = "-") + + # include SVN revision for development versions of R + # (to avoid sharing platform-specific artefacts with released versions of R) + devel <- + identical(R.version[["status"]], "Under development (unstable)") || + identical(R.version[["nickname"]], "Unsuffered Consequences") + + if (devel) + prefix <- paste(prefix, R.version[["svn rev"]], sep = "-r") + + # build list of path components + components <- c(prefix, R.version$platform) + + # include prefix if provided by user + prefix <- renv_bootstrap_platform_prefix_impl() + if (!is.na(prefix) && nzchar(prefix)) + components <- c(prefix, components) + + # build prefix + paste(components, collapse = "/") + + } + + renv_bootstrap_platform_prefix_impl <- function() { + + # if an explicit prefix has been supplied, use it + prefix <- Sys.getenv("RENV_PATHS_PREFIX", unset = NA) + if (!is.na(prefix)) + return(prefix) + + # if the user has requested an automatic prefix, generate it + auto <- Sys.getenv("RENV_PATHS_PREFIX_AUTO", unset = NA) + if (auto %in% c("TRUE", "True", "true", "1")) + return(renv_bootstrap_platform_prefix_auto()) + + # empty string on failure + "" + + } + + renv_bootstrap_platform_prefix_auto <- function() { + + prefix <- tryCatch(renv_bootstrap_platform_os(), error = identity) + if (inherits(prefix, "error") || prefix %in% "unknown") { + + msg <- paste( + "failed to infer current operating system", + "please file a bug report at https://github.com/rstudio/renv/issues", + sep = "; " + ) + + warning(msg) + + } + + prefix + + } + + renv_bootstrap_platform_os <- function() { + + sysinfo <- Sys.info() + sysname <- sysinfo[["sysname"]] + + # handle Windows + macOS up front + if (sysname == "Windows") + return("windows") + else if (sysname == "Darwin") + return("macos") + + # check for os-release files + for (file in c("/etc/os-release", "/usr/lib/os-release")) + if (file.exists(file)) + return(renv_bootstrap_platform_os_via_os_release(file, sysinfo)) + + # check for redhat-release files + if (file.exists("/etc/redhat-release")) + return(renv_bootstrap_platform_os_via_redhat_release()) + + "unknown" + + } + + renv_bootstrap_platform_os_via_os_release <- function(file, sysinfo) { + + # read /etc/os-release + release <- utils::read.table( + file = file, + sep = "=", + quote = c("\"", "'"), + col.names = c("Key", "Value"), + comment.char = "#", + stringsAsFactors = FALSE + ) + + vars <- as.list(release$Value) + names(vars) <- release$Key + + # get os name + os <- tolower(sysinfo[["sysname"]]) + + # read id + id <- "unknown" + for (field in c("ID", "ID_LIKE")) { + if (field %in% names(vars) && nzchar(vars[[field]])) { + id <- vars[[field]] + break + } + } + + # read version + version <- "unknown" + for (field in c("UBUNTU_CODENAME", "VERSION_CODENAME", "VERSION_ID", "BUILD_ID")) { + if (field %in% names(vars) && nzchar(vars[[field]])) { + version <- vars[[field]] + break + } + } + + # join together + paste(c(os, id, version), collapse = "-") + + } + + renv_bootstrap_platform_os_via_redhat_release <- function() { + + # read /etc/redhat-release + contents <- readLines("/etc/redhat-release", warn = FALSE) + + # infer id + id <- if (grepl("centos", contents, ignore.case = TRUE)) + "centos" + else if (grepl("redhat", contents, ignore.case = TRUE)) + "redhat" + else + "unknown" + + # try to find a version component (very hacky) + version <- "unknown" + + parts <- strsplit(contents, "[[:space:]]")[[1L]] + for (part in parts) { + + nv <- tryCatch(numeric_version(part), error = identity) + if (inherits(nv, "error")) + next + + version <- nv[1, 1] + break + + } + + paste(c("linux", id, version), collapse = "-") + + } + + renv_bootstrap_library_root_name <- function(project) { + + # use project name as-is if requested + asis <- Sys.getenv("RENV_PATHS_LIBRARY_ROOT_ASIS", unset = "FALSE") + if (asis) + return(basename(project)) + + # otherwise, disambiguate based on project's path + id <- substring(renv_bootstrap_hash_text(project), 1L, 8L) + paste(basename(project), id, sep = "-") + + } + + renv_bootstrap_library_root <- function(project) { + + prefix <- renv_bootstrap_profile_prefix() + + path <- Sys.getenv("RENV_PATHS_LIBRARY", unset = NA) + if (!is.na(path)) + return(paste(c(path, prefix), collapse = "/")) + + path <- renv_bootstrap_library_root_impl(project) + if (!is.null(path)) { + name <- renv_bootstrap_library_root_name(project) + return(paste(c(path, prefix, name), collapse = "/")) + } + + renv_bootstrap_paths_renv("library", project = project) + + } + + renv_bootstrap_library_root_impl <- function(project) { + + root <- Sys.getenv("RENV_PATHS_LIBRARY_ROOT", unset = NA) + if (!is.na(root)) + return(root) + + type <- renv_bootstrap_project_type(project) + if (identical(type, "package")) { + userdir <- renv_bootstrap_user_dir() + return(file.path(userdir, "library")) + } + + } + + renv_bootstrap_validate_version <- function(version, description = NULL) { + + # resolve description file + # + # avoid passing lib.loc to `packageDescription()` below, since R will + # use the loaded version of the package by default anyhow. note that + # this function should only be called after 'renv' is loaded + # https://github.com/rstudio/renv/issues/1625 + description <- description %||% packageDescription("renv") + + # check whether requested version 'version' matches loaded version of renv + sha <- attr(version, "sha", exact = TRUE) + valid <- if (!is.null(sha)) + renv_bootstrap_validate_version_dev(sha, description) + else + renv_bootstrap_validate_version_release(version, description) + + if (valid) + return(TRUE) + + # the loaded version of renv doesn't match the requested version; + # give the user instructions on how to proceed + remote <- if (!is.null(description[["RemoteSha"]])) { + paste("rstudio/renv", description[["RemoteSha"]], sep = "@") + } else { + paste("renv", description[["Version"]], sep = "@") + } + + # display both loaded version + sha if available + friendly <- renv_bootstrap_version_friendly( + version = description[["Version"]], + sha = description[["RemoteSha"]] + ) + + fmt <- paste( + "renv %1$s was loaded from project library, but this project is configured to use renv %2$s.", + "- Use `renv::record(\"%3$s\")` to record renv %1$s in the lockfile.", + "- Use `renv::restore(packages = \"renv\")` to install renv %2$s into the project library.", + sep = "\n" + ) + catf(fmt, friendly, renv_bootstrap_version_friendly(version), remote) + + FALSE + + } + + renv_bootstrap_validate_version_dev <- function(version, description) { + expected <- description[["RemoteSha"]] + is.character(expected) && startswith(expected, version) + } + + renv_bootstrap_validate_version_release <- function(version, description) { + expected <- description[["Version"]] + is.character(expected) && identical(expected, version) + } + + renv_bootstrap_hash_text <- function(text) { + + hashfile <- tempfile("renv-hash-") + on.exit(unlink(hashfile), add = TRUE) + + writeLines(text, con = hashfile) + tools::md5sum(hashfile) + + } + + renv_bootstrap_load <- function(project, libpath, version) { + + # try to load renv from the project library + if (!requireNamespace("renv", lib.loc = libpath, quietly = TRUE)) + return(FALSE) + + # warn if the version of renv loaded does not match + renv_bootstrap_validate_version(version) + + # execute renv load hooks, if any + hooks <- getHook("renv::autoload") + for (hook in hooks) + if (is.function(hook)) + tryCatch(hook(), error = warnify) + + # load the project + renv::load(project) + + TRUE + + } + + renv_bootstrap_profile_load <- function(project) { + + # if RENV_PROFILE is already set, just use that + profile <- Sys.getenv("RENV_PROFILE", unset = NA) + if (!is.na(profile) && nzchar(profile)) + return(profile) + + # check for a profile file (nothing to do if it doesn't exist) + path <- renv_bootstrap_paths_renv("profile", profile = FALSE, project = project) + if (!file.exists(path)) + return(NULL) + + # read the profile, and set it if it exists + contents <- readLines(path, warn = FALSE) + if (length(contents) == 0L) + return(NULL) + + # set RENV_PROFILE + profile <- contents[[1L]] + if (!profile %in% c("", "default")) + Sys.setenv(RENV_PROFILE = profile) + + profile + + } + + renv_bootstrap_profile_prefix <- function() { + profile <- renv_bootstrap_profile_get() + if (!is.null(profile)) + return(file.path("profiles", profile, "renv")) + } + + renv_bootstrap_profile_get <- function() { + profile <- Sys.getenv("RENV_PROFILE", unset = "") + renv_bootstrap_profile_normalize(profile) + } + + renv_bootstrap_profile_set <- function(profile) { + profile <- renv_bootstrap_profile_normalize(profile) + if (is.null(profile)) + Sys.unsetenv("RENV_PROFILE") + else + Sys.setenv(RENV_PROFILE = profile) + } + + renv_bootstrap_profile_normalize <- function(profile) { + + if (is.null(profile) || profile %in% c("", "default")) + return(NULL) + + profile + + } + + renv_bootstrap_path_absolute <- function(path) { + + substr(path, 1L, 1L) %in% c("~", "/", "\\") || ( + substr(path, 1L, 1L) %in% c(letters, LETTERS) && + substr(path, 2L, 3L) %in% c(":/", ":\\") + ) + + } + + renv_bootstrap_paths_renv <- function(..., profile = TRUE, project = NULL) { + renv <- Sys.getenv("RENV_PATHS_RENV", unset = "renv") + root <- if (renv_bootstrap_path_absolute(renv)) NULL else project + prefix <- if (profile) renv_bootstrap_profile_prefix() + components <- c(root, renv, prefix, ...) + paste(components, collapse = "/") + } + + renv_bootstrap_project_type <- function(path) { + + descpath <- file.path(path, "DESCRIPTION") + if (!file.exists(descpath)) + return("unknown") + + desc <- tryCatch( + read.dcf(descpath, all = TRUE), + error = identity + ) + + if (inherits(desc, "error")) + return("unknown") + + type <- desc$Type + if (!is.null(type)) + return(tolower(type)) + + package <- desc$Package + if (!is.null(package)) + return("package") + + "unknown" + + } + + renv_bootstrap_user_dir <- function() { + dir <- renv_bootstrap_user_dir_impl() + path.expand(chartr("\\", "/", dir)) + } + + renv_bootstrap_user_dir_impl <- function() { + + # use local override if set + override <- getOption("renv.userdir.override") + if (!is.null(override)) + return(override) + + # use R_user_dir if available + tools <- asNamespace("tools") + if (is.function(tools$R_user_dir)) + return(tools$R_user_dir("renv", "cache")) + + # try using our own backfill for older versions of R + envvars <- c("R_USER_CACHE_DIR", "XDG_CACHE_HOME") + for (envvar in envvars) { + root <- Sys.getenv(envvar, unset = NA) + if (!is.na(root)) + return(file.path(root, "R/renv")) + } + + # use platform-specific default fallbacks + if (Sys.info()[["sysname"]] == "Windows") + file.path(Sys.getenv("LOCALAPPDATA"), "R/cache/R/renv") + else if (Sys.info()[["sysname"]] == "Darwin") + "~/Library/Caches/org.R-project.R/R/renv" + else + "~/.cache/R/renv" + + } + + renv_bootstrap_version_friendly <- function(version, shafmt = NULL, sha = NULL) { + sha <- sha %||% attr(version, "sha", exact = TRUE) + parts <- c(version, sprintf(shafmt %||% " [sha: %s]", substring(sha, 1L, 7L))) + paste(parts, collapse = "") + } + + renv_bootstrap_exec <- function(project, libpath, version) { + if (!renv_bootstrap_load(project, libpath, version)) + renv_bootstrap_run(version, libpath) + } + + renv_bootstrap_run <- function(version, libpath) { + + # perform bootstrap + bootstrap(version, libpath) + + # exit early if we're just testing bootstrap + if (!is.na(Sys.getenv("RENV_BOOTSTRAP_INSTALL_ONLY", unset = NA))) + return(TRUE) + + # try again to load + if (requireNamespace("renv", lib.loc = libpath, quietly = TRUE)) { + return(renv::load(project = getwd())) + } + + # failed to download or load renv; warn the user + msg <- c( + "Failed to find an renv installation: the project will not be loaded.", + "Use `renv::activate()` to re-initialize the project." + ) + + warning(paste(msg, collapse = "\n"), call. = FALSE) + + } + + renv_json_read <- function(file = NULL, text = NULL) { + + jlerr <- NULL + + # if jsonlite is loaded, use that instead + if ("jsonlite" %in% loadedNamespaces()) { + + json <- catch(renv_json_read_jsonlite(file, text)) + if (!inherits(json, "error")) + return(json) + + jlerr <- json + + } + + # otherwise, fall back to the default JSON reader + json <- catch(renv_json_read_default(file, text)) + if (!inherits(json, "error")) + return(json) + + # report an error + if (!is.null(jlerr)) + stop(jlerr) + else + stop(json) + + } + + renv_json_read_jsonlite <- function(file = NULL, text = NULL) { + text <- paste(text %||% read(file), collapse = "\n") + jsonlite::fromJSON(txt = text, simplifyVector = FALSE) + } + + renv_json_read_default <- function(file = NULL, text = NULL) { + + # find strings in the JSON + text <- paste(text %||% read(file), collapse = "\n") + pattern <- '["](?:(?:\\\\.)|(?:[^"\\\\]))*?["]' + locs <- gregexpr(pattern, text, perl = TRUE)[[1]] + + # if any are found, replace them with placeholders + replaced <- text + strings <- character() + replacements <- character() + + if (!identical(c(locs), -1L)) { + + # get the string values + starts <- locs + ends <- locs + attr(locs, "match.length") - 1L + strings <- substring(text, starts, ends) + + # only keep those requiring escaping + strings <- grep("[[\\]{}:]", strings, perl = TRUE, value = TRUE) + + # compute replacements + replacements <- sprintf('"\032%i\032"', seq_along(strings)) + + # replace the strings + mapply(function(string, replacement) { + replaced <<- sub(string, replacement, replaced, fixed = TRUE) + }, strings, replacements) + + } + + # transform the JSON into something the R parser understands + transformed <- replaced + transformed <- gsub("{}", "`names<-`(list(), character())", transformed, fixed = TRUE) + transformed <- gsub("[[{]", "list(", transformed, perl = TRUE) + transformed <- gsub("[]}]", ")", transformed, perl = TRUE) + transformed <- gsub(":", "=", transformed, fixed = TRUE) + text <- paste(transformed, collapse = "\n") + + # parse it + json <- parse(text = text, keep.source = FALSE, srcfile = NULL)[[1L]] + + # construct map between source strings, replaced strings + map <- as.character(parse(text = strings)) + names(map) <- as.character(parse(text = replacements)) + + # convert to list + map <- as.list(map) + + # remap strings in object + remapped <- renv_json_remap(json, map) + + # evaluate + eval(remapped, envir = baseenv()) + + } + + renv_json_remap <- function(json, map) { + + # fix names + if (!is.null(names(json))) { + lhs <- match(names(json), names(map), nomatch = 0L) + rhs <- match(names(map), names(json), nomatch = 0L) + names(json)[rhs] <- map[lhs] + } + + # fix values + if (is.character(json)) + return(map[[json]] %||% json) + + # handle true, false, null + if (is.name(json)) { + text <- as.character(json) + if (text == "true") + return(TRUE) + else if (text == "false") + return(FALSE) + else if (text == "null") + return(NULL) + } + + # recurse + if (is.recursive(json)) { + for (i in seq_along(json)) { + json[i] <- list(renv_json_remap(json[[i]], map)) + } + } + + json + + } + + # load the renv profile, if any + renv_bootstrap_profile_load(project) + + # construct path to library root + root <- renv_bootstrap_library_root(project) + + # construct library prefix for platform + prefix <- renv_bootstrap_platform_prefix() + + # construct full libpath + libpath <- file.path(root, prefix) + + # run bootstrap code + renv_bootstrap_exec(project, libpath, version) + + invisible() + +}) diff --git a/renv/settings.json b/renv/settings.json new file mode 100644 index 0000000..ffdbb32 --- /dev/null +++ b/renv/settings.json @@ -0,0 +1,19 @@ +{ + "bioconductor.version": null, + "external.libraries": [], + "ignored.packages": [], + "package.dependency.fields": [ + "Imports", + "Depends", + "LinkingTo" + ], + "ppm.enabled": null, + "ppm.ignored.urls": [], + "r.version": null, + "snapshot.type": "implicit", + "use.cache": true, + "vcs.ignore.cellar": true, + "vcs.ignore.library": true, + "vcs.ignore.local": true, + "vcs.manage.ignores": true +} diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..b495000 --- /dev/null +++ b/uv.lock @@ -0,0 +1,880 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "click" +version = "8.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "colorcet" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/af/b969f541242b84cbbacabdf20862e487689e352bf0f02f90df2795d29da5/colorcet-3.2.1.tar.gz", hash = "sha256:48d9a67e6e59dc5c0a965aa1b46fe5d59cdc95cc36a95949f29313f950ac59f7", size = 2202958, upload-time = "2026-04-28T16:25:37.43Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/24/e95471ae93c08d3606c9c7343cf65d490f154daa88b50581957a0aa780f4/colorcet-3.2.1-py3-none-any.whl", hash = "sha256:3f6fde13cef2169222dd5fe2a2bf847c02d644470fdf167ed566f6421df470f7", size = 262291, upload-time = "2026-04-28T16:25:35.365Z" }, +] + +[[package]] +name = "h5py" +version = "3.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/33/acd0ce6863b6c0d7735007df01815403f5589a21ff8c2e1ee2587a38f548/h5py-3.16.0.tar.gz", hash = "sha256:a0dbaad796840ccaa67a4c144a0d0c8080073c34c76d5a6941d6818678ef2738", size = 446526, upload-time = "2026-03-06T13:49:08.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/c0/5d4119dba94093bbafede500d3defd2f5eab7897732998c04b54021e530b/h5py-3.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c5313566f4643121a78503a473f0fb1e6dcc541d5115c44f05e037609c565c4d", size = 3685604, upload-time = "2026-03-06T13:48:04.198Z" }, + { url = "https://files.pythonhosted.org/packages/b0/42/c84efcc1d4caebafb1ecd8be4643f39c85c47a80fe254d92b8b43b1eadaf/h5py-3.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:42b012933a83e1a558c673176676a10ce2fd3759976a0fedee1e672d1e04fc9d", size = 3061940, upload-time = "2026-03-06T13:48:05.783Z" }, + { url = "https://files.pythonhosted.org/packages/89/84/06281c82d4d1686fde1ac6b0f307c50918f1c0151062445ab3b6fa5a921d/h5py-3.16.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:ff24039e2573297787c3063df64b60aab0591980ac898329a08b0320e0cf2527", size = 5198852, upload-time = "2026-03-06T13:48:07.482Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e9/1a19e42cd43cc1365e127db6aae85e1c671da1d9a5d746f4d34a50edb577/h5py-3.16.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:dfc21898ff025f1e8e67e194965a95a8d4754f452f83454538f98f8a3fcb207e", size = 5405250, upload-time = "2026-03-06T13:48:09.628Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8e/9790c1655eabeb85b92b1ecab7d7e62a2069e53baefd58c98f0909c7a948/h5py-3.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:698dd69291272642ffda44a0ecd6cd3bda5faf9621452d255f57ce91487b9794", size = 5190108, upload-time = "2026-03-06T13:48:11.26Z" }, + { url = "https://files.pythonhosted.org/packages/51/d7/ab693274f1bd7e8c5f9fdd6c7003a88d59bedeaf8752716a55f532924fbb/h5py-3.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2b2c02b0a160faed5fb33f1ba8a264a37ee240b22e049ecc827345d0d9043074", size = 5419216, upload-time = "2026-03-06T13:48:13.322Z" }, + { url = "https://files.pythonhosted.org/packages/03/c1/0976b235cf29ead553e22f2fb6385a8252b533715e00d0ae52ed7b900582/h5py-3.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:96b422019a1c8975c2d5dadcf61d4ba6f01c31f92bbde6e4649607885fe502d6", size = 3182868, upload-time = "2026-03-06T13:48:15.759Z" }, + { url = "https://files.pythonhosted.org/packages/14/d9/866b7e570b39070f92d47b0ff1800f0f8239b6f9e45f02363d7112336c1f/h5py-3.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:39c2838fb1e8d97bcf1755e60ad1f3dd76a7b2a475928dc321672752678b96db", size = 2653286, upload-time = "2026-03-06T13:48:17.279Z" }, + { url = "https://files.pythonhosted.org/packages/0f/9e/6142ebfda0cb6e9349c091eae73c2e01a770b7659255248d637bec54a88b/h5py-3.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:370a845f432c2c9619db8eed334d1e610c6015796122b0e57aa46312c22617d9", size = 3671808, upload-time = "2026-03-06T13:48:19.737Z" }, + { url = "https://files.pythonhosted.org/packages/b0/65/5e088a45d0f43cd814bc5bec521c051d42005a472e804b1a36c48dada09b/h5py-3.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42108e93326c50c2810025aade9eac9d6827524cdccc7d4b75a546e5ab308edb", size = 3045837, upload-time = "2026-03-06T13:48:21.854Z" }, + { url = "https://files.pythonhosted.org/packages/da/1e/6172269e18cc5a484e2913ced33339aad588e02ba407fafd00d369e22ef3/h5py-3.16.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:099f2525c9dcf28de366970a5fb34879aab20491589fa89ce2863a84218bb524", size = 5193860, upload-time = "2026-03-06T13:48:24.071Z" }, + { url = "https://files.pythonhosted.org/packages/bd/98/ef2b6fe2903e377cbe870c3b2800d62552f1e3dbe81ce49e1923c53d1c5c/h5py-3.16.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:9300ad32dea9dfc5171f94d5f6948e159ed93e4701280b0f508773b3f582f402", size = 5400417, upload-time = "2026-03-06T13:48:25.728Z" }, + { url = "https://files.pythonhosted.org/packages/bc/81/5b62d760039eed64348c98129d17061fdfc7839fc9c04eaaad6dee1004e4/h5py-3.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:171038f23bccddfc23f344cadabdfc9917ff554db6a0d417180d2747fe4c75a7", size = 5185214, upload-time = "2026-03-06T13:48:27.436Z" }, + { url = "https://files.pythonhosted.org/packages/28/c4/532123bcd9080e250696779c927f2cb906c8bf3447df98f5ceb8dcded539/h5py-3.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7e420b539fb6023a259a1b14d4c9f6df8cf50d7268f48e161169987a57b737ff", size = 5414598, upload-time = "2026-03-06T13:48:29.49Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d9/a27997f84341fc0dfcdd1fe4179b6ba6c32a7aa880fdb8c514d4dad6fba3/h5py-3.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:18f2bbcd545e6991412253b98727374c356d67caa920e68dc79eab36bf5fedad", size = 3175509, upload-time = "2026-03-06T13:48:31.131Z" }, + { url = "https://files.pythonhosted.org/packages/a5/23/bb8647521d4fd770c30a76cfc6cb6a2f5495868904054e92f2394c5a78ff/h5py-3.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:656f00e4d903199a1d58df06b711cf3ca632b874b4207b7dbec86185b5c8c7d4", size = 2647362, upload-time = "2026-03-06T13:48:33.411Z" }, + { url = "https://files.pythonhosted.org/packages/48/3c/7fcd9b4c9eed82e91fb15568992561019ae7a829d1f696b2c844355d95dd/h5py-3.16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9c9d307c0ef862d1cd5714f72ecfafe0a5d7529c44845afa8de9f46e5ba8bd65", size = 3678608, upload-time = "2026-03-06T13:48:35.183Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b7/9366ed44ced9b7ef357ab48c94205280276db9d7f064aa3012a97227e966/h5py-3.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8c1eff849cdd53cbc73c214c30ebdb6f1bb8b64790b4b4fc36acdb5e43570210", size = 3054773, upload-time = "2026-03-06T13:48:37.139Z" }, + { url = "https://files.pythonhosted.org/packages/58/a5/4964bc0e91e86340c2bbda83420225b2f770dcf1eb8a39464871ad769436/h5py-3.16.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:e2c04d129f180019e216ee5f9c40b78a418634091c8782e1f723a6ca3658b965", size = 5198886, upload-time = "2026-03-06T13:48:38.879Z" }, + { url = "https://files.pythonhosted.org/packages/f1/16/d905e7f53e661ce2c24686c38048d8e2b750ffc4350009d41c4e6c6c9826/h5py-3.16.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:e4360f15875a532bc7b98196c7592ed4fc92672a57c0a621355961cafb17a6dd", size = 5404883, upload-time = "2026-03-06T13:48:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f2/58f34cb74af46d39f4cd18ea20909a8514960c5a3e5b92fd06a28161e0a8/h5py-3.16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3fae9197390c325e62e0a1aa977f2f62d994aa87aab182abbea85479b791197c", size = 5192039, upload-time = "2026-03-06T13:48:43.117Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ca/934a39c24ce2e2db017268c08da0537c20fa0be7e1549be3e977313fc8f5/h5py-3.16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:43259303989ac8adacc9986695b31e35dba6fd1e297ff9c6a04b7da5542139cc", size = 5421526, upload-time = "2026-03-06T13:48:44.838Z" }, + { url = "https://files.pythonhosted.org/packages/3e/14/615a450205e1b56d16c6783f5ccd116cde05550faad70ae077c955654a75/h5py-3.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:fa48993a0b799737ba7fd21e2350fa0a60701e58180fae9f2de834bc39a147ab", size = 3183263, upload-time = "2026-03-06T13:48:47.117Z" }, + { url = "https://files.pythonhosted.org/packages/7b/48/a6faef5ed632cae0c65ac6b214a6614a0b510c3183532c521bdb0055e117/h5py-3.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:1897a771a7f40d05c262fc8f37376ec37873218544b70216872876c627640f63", size = 2663450, upload-time = "2026-03-06T13:48:48.707Z" }, + { url = "https://files.pythonhosted.org/packages/5d/32/0c8bb8aedb62c772cf7c1d427c7d1951477e8c2835f872bc0a13d1f85f86/h5py-3.16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:15922e485844f77c0b9d275396d435db3baa58292a9c2176a386e072e0cf2491", size = 3760693, upload-time = "2026-03-06T13:48:50.453Z" }, + { url = "https://files.pythonhosted.org/packages/1d/1f/fcc5977d32d6387c5c9a694afee716a5e20658ac08b3ff24fdec79fb05f2/h5py-3.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:df02dd29bd247f98674634dfe41f89fd7c16ba3d7de8695ec958f58404a4e618", size = 3181305, upload-time = "2026-03-06T13:48:52.221Z" }, + { url = "https://files.pythonhosted.org/packages/f5/a1/af87f64b9f986889884243643621ebbd4ac72472ba8ec8cec891ac8e2ca1/h5py-3.16.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:0f456f556e4e2cebeebd9d66adf8dc321770a42593494a0b6f0af54a7567b242", size = 5074061, upload-time = "2026-03-06T13:48:54.089Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d0/146f5eaff3dc246a9c7f6e5e4f42bd45cc613bce16693bcd4d1f7c958bf5/h5py-3.16.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:3e6cb3387c756de6a9492d601553dffea3fe11b5f22b443aac708c69f3f55e16", size = 5279216, upload-time = "2026-03-06T13:48:56.75Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9d/12a13424f1e604fc7df9497b73c0356fb78c2fb206abd7465ce47226e8fd/h5py-3.16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8389e13a1fd745ad2856873e8187fd10268b2d9677877bb667b41aebd771d8b7", size = 5070068, upload-time = "2026-03-06T13:48:59.169Z" }, + { url = "https://files.pythonhosted.org/packages/41/8c/bbe98f813722b4873818a8db3e15aa3e625b59278566905ac439725e8070/h5py-3.16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:346df559a0f7dcb31cf8e44805319e2ab24b8957c45e7708ce503b2ec79ba725", size = 5300253, upload-time = "2026-03-06T13:49:02.033Z" }, + { url = "https://files.pythonhosted.org/packages/32/9e/87e6705b4d6890e7cecdf876e2a7d3e40654a2ae37482d79a6f1b87f7b92/h5py-3.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4c6ab014ab704b4feaa719ae783b86522ed0bf1f82184704ed3c9e4e3228796e", size = 3381671, upload-time = "2026-03-06T13:49:04.351Z" }, + { url = "https://files.pythonhosted.org/packages/96/91/9fad90cfc5f9b2489c7c26ad897157bce82f0e9534a986a221b99760b23b/h5py-3.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:faca8fb4e4319c09d83337adc80b2ca7d5c5a343c2d6f1b6388f32cfecca13c1", size = 2740706, upload-time = "2026-03-06T13:49:06.347Z" }, +] + +[[package]] +name = "hdmf" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h5py" }, + { name = "jsonschema" }, + { name = "numpy" }, + { name = "pandas" }, + { name = "ruamel-yaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/79/13daab608114a550ef53433d1718a923d01c9d9b0f1e713bf2b9c3f98c69/hdmf-4.2.0.tar.gz", hash = "sha256:3555fb39874544ec3cd741b9eccd8db2339fae000e5a51859e5c20c3b0c6d4b5", size = 16618478, upload-time = "2025-12-18T18:49:18.707Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/c4/2200240c6318b2057162335f77697011c07e85275ec64f11212fdeb7fd38/hdmf-4.2.0-py3-none-any.whl", hash = "sha256:d2856ebdd6058ff50d09ec226d65d72617919dd9e7ebcd6912c7e0917600f319", size = 340400, upload-time = "2025-12-18T18:49:16.507Z" }, +] + +[[package]] +name = "imageio" +version = "2.37.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/84/93bcd1300216ea50811cee96873b84a1bebf8d0489ffaf7f2a3756bab866/imageio-2.37.3.tar.gz", hash = "sha256:bbb37efbfc4c400fcd534b367b91fcd66d5da639aaa138034431a1c5e0a41451", size = 389673, upload-time = "2026-03-09T11:31:12.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl", hash = "sha256:46f5bb8522cd421c0f5ae104d8268f569d856b29eb1a13b92829d1970f32c9f0", size = 317646, upload-time = "2026-03-09T11:31:10.771Z" }, +] + +[[package]] +name = "imageio-ffmpeg" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/bd/c3343c721f2a1b0c9fc71c1aebf1966a3b7f08c2eea8ed5437a2865611d6/imageio_ffmpeg-0.6.0.tar.gz", hash = "sha256:e2556bed8e005564a9f925bb7afa4002d82770d6b08825078b7697ab88ba1755", size = 25210, upload-time = "2025-01-16T21:34:32.747Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/58/87ef68ac83f4c7690961bce288fd8e382bc5f1513860fc7f90a9c1c1c6bf/imageio_ffmpeg-0.6.0-py3-none-macosx_10_9_intel.macosx_10_9_x86_64.whl", hash = "sha256:9d2baaf867088508d4a3458e61eeb30e945c4ad8016025545f66c4b5aaef0a61", size = 24932969, upload-time = "2025-01-16T21:34:20.464Z" }, + { url = "https://files.pythonhosted.org/packages/40/5c/f3d8a657d362cc93b81aab8feda487317da5b5d31c0e1fdfd5e986e55d17/imageio_ffmpeg-0.6.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b1ae3173414b5fc5f538a726c4e48ea97edc0d2cdc11f103afee655c463fa742", size = 21113891, upload-time = "2025-01-16T21:34:00.277Z" }, + { url = "https://files.pythonhosted.org/packages/33/e7/1925bfbc563c39c1d2e82501d8372734a5c725e53ac3b31b4c2d081e895b/imageio_ffmpeg-0.6.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:1d47bebd83d2c5fc770720d211855f208af8a596c82d17730aa51e815cdee6dc", size = 25632706, upload-time = "2025-01-16T21:33:53.475Z" }, + { url = "https://files.pythonhosted.org/packages/a0/2d/43c8522a2038e9d0e7dbdf3a61195ecc31ca576fb1527a528c877e87d973/imageio_ffmpeg-0.6.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:c7e46fcec401dd990405049d2e2f475e2b397779df2519b544b8aab515195282", size = 29498237, upload-time = "2025-01-16T21:34:13.726Z" }, + { url = "https://files.pythonhosted.org/packages/a0/13/59da54728351883c3c1d9fca1710ab8eee82c7beba585df8f25ca925f08f/imageio_ffmpeg-0.6.0-py3-none-win32.whl", hash = "sha256:196faa79366b4a82f95c0f4053191d2013f4714a715780f0ad2a68ff37483cc2", size = 19652251, upload-time = "2025-01-16T21:34:06.812Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c6/fa760e12a2483469e2bf5058c5faff664acf66cadb4df2ad6205b016a73d/imageio_ffmpeg-0.6.0-py3-none-win_amd64.whl", hash = "sha256:02fa47c83703c37df6bfe4896aab339013f62bf02c5ebf2dce6da56af04ffc0a", size = 31246824, upload-time = "2025-01-16T21:34:28.6Z" }, +] + +[[package]] +name = "jabs-nextflow-postprocess" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "h5py" }, + { name = "sleap-io" }, +] + +[package.metadata] +requires-dist = [ + { name = "h5py", specifier = ">=3.16.0" }, + { name = "sleap-io", specifier = ">=0.7.0" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "lazy-loader" +version = "0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/ac/21a1f8aa3777f5658576777ea76bfb124b702c520bbe90edf4ae9915eafa/lazy_loader-0.5.tar.gz", hash = "sha256:717f9179a0dbed357012ddad50a5ad3d5e4d9a0b8712680d4e687f5e6e6ed9b3", size = 15294, upload-time = "2026-03-06T15:45:09.054Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl", hash = "sha256:ab0ea149e9c554d4ffeeb21105ac60bed7f3b4fd69b1d2360a4add51b170b005", size = 8044, upload-time = "2026-03-06T15:45:07.668Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "ndx-multisubjects" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "hdmf" }, + { name = "pynwb" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/42/f6bfac40d886234b522dde62e5e8d0c793edbbb9129265d54b834292f9ad/ndx_multisubjects-0.1.1.tar.gz", hash = "sha256:eec83f2913ca19b99563ea26aa036bc6663611632e868984fff5359d6f018f34", size = 19421, upload-time = "2025-11-25T17:01:36.999Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/74/80892f54af53f3d70eff30e3d43844e4e1f50080fa40e949d5f714d40359/ndx_multisubjects-0.1.1-py3-none-any.whl", hash = "sha256:6f1b766e7211434a5a70ee263232a7581de03cb0cb9364989c59fe589828ad1a", size = 7677, upload-time = "2025-11-25T17:01:35.518Z" }, +] + +[[package]] +name = "ndx-pose" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "hdmf" }, + { name = "pynwb" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/9a/137b1334efc99d8e044948b7d1c52b3ef0f6c44d39654ced1d5236a2af14/ndx_pose-0.2.2.tar.gz", hash = "sha256:69a9a139363ccf8406f36752146d345914330e69921b9734db2425fc9808a0a1", size = 99404, upload-time = "2025-05-08T15:34:38.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/0d/14e1242b3776a06ad6f93107f147e0d0630221c13a1f5e328fba62d95adf/ndx_pose-0.2.2-py3-none-any.whl", hash = "sha256:76d55ec5d363673b66cd9540dac87c92cba16f5def9836874fd4d96b29a94b90", size = 15045, upload-time = "2025-05-08T15:34:37.396Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/05/32396bec30fb2263770ee910142f49c1476d08e8ad41abf8403806b520ce/numpy-2.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b", size = 16689272, upload-time = "2026-03-29T13:18:49.223Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f3/a983d28637bfcd763a9c7aafdb6d5c0ebf3d487d1e1459ffdb57e2f01117/numpy-2.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e", size = 14699573, upload-time = "2026-03-29T13:18:52.629Z" }, + { url = "https://files.pythonhosted.org/packages/9b/fd/e5ecca1e78c05106d98028114f5c00d3eddb41207686b2b7de3e477b0e22/numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842", size = 5204782, upload-time = "2026-03-29T13:18:55.579Z" }, + { url = "https://files.pythonhosted.org/packages/de/2f/702a4594413c1a8632092beae8aba00f1d67947389369b3777aed783fdca/numpy-2.4.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8", size = 6552038, upload-time = "2026-03-29T13:18:57.769Z" }, + { url = "https://files.pythonhosted.org/packages/7f/37/eed308a8f56cba4d1fdf467a4fc67ef4ff4bf1c888f5fc980481890104b1/numpy-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121", size = 15670666, upload-time = "2026-03-29T13:19:00.341Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0d/0e3ecece05b7a7e87ab9fb587855548da437a061326fff64a223b6dcb78a/numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e", size = 16645480, upload-time = "2026-03-29T13:19:03.63Z" }, + { url = "https://files.pythonhosted.org/packages/34/49/f2312c154b82a286758ee2f1743336d50651f8b5195db18cdb63675ff649/numpy-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44", size = 17020036, upload-time = "2026-03-29T13:19:07.428Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e9/736d17bd77f1b0ec4f9901aaec129c00d59f5d84d5e79bba540ef12c2330/numpy-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d", size = 18368643, upload-time = "2026-03-29T13:19:10.775Z" }, + { url = "https://files.pythonhosted.org/packages/63/f6/d417977c5f519b17c8a5c3bc9e8304b0908b0e21136fe43bf628a1343914/numpy-2.4.4-cp312-cp312-win32.whl", hash = "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827", size = 5961117, upload-time = "2026-03-29T13:19:13.464Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5b/e1deebf88ff431b01b7406ca3583ab2bbb90972bbe1c568732e49c844f7e/numpy-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a", size = 12320584, upload-time = "2026-03-29T13:19:16.155Z" }, + { url = "https://files.pythonhosted.org/packages/58/89/e4e856ac82a68c3ed64486a544977d0e7bdd18b8da75b78a577ca31c4395/numpy-2.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec", size = 10221450, upload-time = "2026-03-29T13:19:18.994Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d0a583ce4fefcc3308806a749a536c201ed6b5ad6e1322e227ee4848979d/numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50", size = 16684933, upload-time = "2026-03-29T13:19:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/c1/62/2b7a48fbb745d344742c0277f01286dead15f3f68e4f359fbfcf7b48f70f/numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115", size = 14694532, upload-time = "2026-03-29T13:19:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af", size = 5199661, upload-time = "2026-03-29T13:19:28.31Z" }, + { url = "https://files.pythonhosted.org/packages/cd/da/464d551604320d1491bc345efed99b4b7034143a85787aab78d5691d5a0e/numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c", size = 6547539, upload-time = "2026-03-29T13:19:30.97Z" }, + { url = "https://files.pythonhosted.org/packages/7d/90/8d23e3b0dafd024bf31bdec225b3bb5c2dbfa6912f8a53b8659f21216cbf/numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103", size = 15668806, upload-time = "2026-03-29T13:19:33.887Z" }, + { url = "https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83", size = 16632682, upload-time = "2026-03-29T13:19:37.336Z" }, + { url = "https://files.pythonhosted.org/packages/34/fb/14570d65c3bde4e202a031210475ae9cde9b7686a2e7dc97ee67d2833b35/numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed", size = 17019810, upload-time = "2026-03-29T13:19:40.963Z" }, + { url = "https://files.pythonhosted.org/packages/8a/77/2ba9d87081fd41f6d640c83f26fb7351e536b7ce6dd9061b6af5904e8e46/numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959", size = 18357394, upload-time = "2026-03-29T13:19:44.859Z" }, + { url = "https://files.pythonhosted.org/packages/a2/23/52666c9a41708b0853fa3b1a12c90da38c507a3074883823126d4e9d5b30/numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed", size = 5959556, upload-time = "2026-03-29T13:19:47.661Z" }, + { url = "https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf", size = 12317311, upload-time = "2026-03-29T13:19:50.67Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d8/11490cddd564eb4de97b4579ef6bfe6a736cc07e94c1598590ae25415e01/numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d", size = 10222060, upload-time = "2026-03-29T13:19:54.229Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/dab4339177a905aad3e2221c915b35202f1ec30d750dd2e5e9d9a72b804b/numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5", size = 14822302, upload-time = "2026-03-29T13:19:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e4/0564a65e7d3d97562ed6f9b0fd0fb0a6f559ee444092f105938b50043876/numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7", size = 5327407, upload-time = "2026-03-29T13:20:00.601Z" }, + { url = "https://files.pythonhosted.org/packages/29/8d/35a3a6ce5ad371afa58b4700f1c820f8f279948cca32524e0a695b0ded83/numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93", size = 6647631, upload-time = "2026-03-29T13:20:02.855Z" }, + { url = "https://files.pythonhosted.org/packages/f4/da/477731acbd5a58a946c736edfdabb2ac5b34c3d08d1ba1a7b437fa0884df/numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e", size = 15727691, upload-time = "2026-03-29T13:20:06.004Z" }, + { url = "https://files.pythonhosted.org/packages/e6/db/338535d9b152beabeb511579598418ba0212ce77cf9718edd70262cc4370/numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40", size = 16681241, upload-time = "2026-03-29T13:20:09.417Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a9/ad248e8f58beb7a0219b413c9c7d8151c5d285f7f946c3e26695bdbbe2df/numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e", size = 17085767, upload-time = "2026-03-29T13:20:13.126Z" }, + { url = "https://files.pythonhosted.org/packages/b5/1a/3b88ccd3694681356f70da841630e4725a7264d6a885c8d442a697e1146b/numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392", size = 18403169, upload-time = "2026-03-29T13:20:17.096Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c9/fcfd5d0639222c6eac7f304829b04892ef51c96a75d479214d77e3ce6e33/numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008", size = 6083477, upload-time = "2026-03-29T13:20:20.195Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e3/3938a61d1c538aaec8ed6fd6323f57b0c2d2d2219512434c5c878db76553/numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8", size = 12457487, upload-time = "2026-03-29T13:20:22.946Z" }, + { url = "https://files.pythonhosted.org/packages/97/6a/7e345032cc60501721ef94e0e30b60f6b0bd601f9174ebd36389a2b86d40/numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233", size = 10292002, upload-time = "2026-03-29T13:20:25.909Z" }, + { url = "https://files.pythonhosted.org/packages/6e/06/c54062f85f673dd5c04cbe2f14c3acb8c8b95e3384869bb8cc9bff8cb9df/numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0", size = 16684353, upload-time = "2026-03-29T13:20:29.504Z" }, + { url = "https://files.pythonhosted.org/packages/4c/39/8a320264a84404c74cc7e79715de85d6130fa07a0898f67fb5cd5bd79908/numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a", size = 14704914, upload-time = "2026-03-29T13:20:33.547Z" }, + { url = "https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a", size = 5210005, upload-time = "2026-03-29T13:20:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/63/eb/fcc338595309910de6ecabfcef2419a9ce24399680bfb149421fa2df1280/numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b", size = 6544974, upload-time = "2026-03-29T13:20:39.014Z" }, + { url = "https://files.pythonhosted.org/packages/44/5d/e7e9044032a716cdfaa3fba27a8e874bf1c5f1912a1ddd4ed071bf8a14a6/numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a", size = 15684591, upload-time = "2026-03-29T13:20:42.146Z" }, + { url = "https://files.pythonhosted.org/packages/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d", size = 16637700, upload-time = "2026-03-29T13:20:46.204Z" }, + { url = "https://files.pythonhosted.org/packages/b1/29/56d2bbef9465db24ef25393383d761a1af4f446a1df9b8cded4fe3a5a5d7/numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252", size = 17035781, upload-time = "2026-03-29T13:20:50.242Z" }, + { url = "https://files.pythonhosted.org/packages/e3/2b/a35a6d7589d21f44cea7d0a98de5ddcbb3d421b2622a5c96b1edf18707c3/numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f", size = 18362959, upload-time = "2026-03-29T13:20:54.019Z" }, + { url = "https://files.pythonhosted.org/packages/64/c9/d52ec581f2390e0f5f85cbfd80fb83d965fc15e9f0e1aec2195faa142cde/numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", size = 6008768, upload-time = "2026-03-29T13:20:56.912Z" }, + { url = "https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", size = 12449181, upload-time = "2026-03-29T13:20:59.548Z" }, + { url = "https://files.pythonhosted.org/packages/70/2e/14cda6f4d8e396c612d1bf97f22958e92148801d7e4f110cabebdc0eef4b/numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", size = 10496035, upload-time = "2026-03-29T13:21:02.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e8/8fed8c8d848d7ecea092dc3469643f9d10bc3a134a815a3b033da1d2039b/numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e", size = 14824958, upload-time = "2026-03-29T13:21:05.671Z" }, + { url = "https://files.pythonhosted.org/packages/05/1a/d8007a5138c179c2bf33ef44503e83d70434d2642877ee8fbb230e7c0548/numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113", size = 5330020, upload-time = "2026-03-29T13:21:08.635Z" }, + { url = "https://files.pythonhosted.org/packages/99/64/ffb99ac6ae93faf117bcbd5c7ba48a7f45364a33e8e458545d3633615dda/numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d", size = 6650758, upload-time = "2026-03-29T13:21:10.949Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6e/795cc078b78a384052e73b2f6281ff7a700e9bf53bcce2ee579d4f6dd879/numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d", size = 15729948, upload-time = "2026-03-29T13:21:14.047Z" }, + { url = "https://files.pythonhosted.org/packages/5f/86/2acbda8cc2af5f3d7bfc791192863b9e3e19674da7b5e533fded124d1299/numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f", size = 16679325, upload-time = "2026-03-29T13:21:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/bc/59/cafd83018f4aa55e0ac6fa92aa066c0a1877b77a615ceff1711c260ffae8/numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0", size = 17084883, upload-time = "2026-03-29T13:21:21.106Z" }, + { url = "https://files.pythonhosted.org/packages/f0/85/a42548db84e65ece46ab2caea3d3f78b416a47af387fcbb47ec28e660dc2/numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150", size = 18403474, upload-time = "2026-03-29T13:21:24.828Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500, upload-time = "2026-03-29T13:21:28.205Z" }, + { url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755, upload-time = "2026-03-29T13:21:31.107Z" }, + { url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pandas" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/99/b342345300f13440fe9fe385c3c481e2d9a595ee3bab4d3219247ac94e9a/pandas-3.0.2.tar.gz", hash = "sha256:f4753e73e34c8d83221ba58f232433fca2748be8b18dbca02d242ed153945043", size = 4645855, upload-time = "2026-03-31T06:48:30.816Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/b0/c20bd4d6d3f736e6bd6b55794e9cd0a617b858eaad27c8f410ea05d953b7/pandas-3.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:232a70ebb568c0c4d2db4584f338c1577d81e3af63292208d615907b698a0f18", size = 10347921, upload-time = "2026-03-31T06:46:33.36Z" }, + { url = "https://files.pythonhosted.org/packages/35/d0/4831af68ce30cc2d03c697bea8450e3225a835ef497d0d70f31b8cdde965/pandas-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:970762605cff1ca0d3f71ed4f3a769ea8f85fc8e6348f6e110b8fea7e6eb5a14", size = 9888127, upload-time = "2026-03-31T06:46:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/61/a9/16ea9346e1fc4a96e2896242d9bc674764fb9049b0044c0132502f7a771e/pandas-3.0.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aff4e6f4d722e0652707d7bcb190c445fe58428500c6d16005b02401764b1b3d", size = 10399577, upload-time = "2026-03-31T06:46:39.224Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a8/3a61a721472959ab0ce865ef05d10b0d6bfe27ce8801c99f33d4fa996e65/pandas-3.0.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef8b27695c3d3dc78403c9a7d5e59a62d5464a7e1123b4e0042763f7104dc74f", size = 10880030, upload-time = "2026-03-31T06:46:42.412Z" }, + { url = "https://files.pythonhosted.org/packages/da/65/7225c0ea4d6ce9cb2160a7fb7f39804871049f016e74782e5dade4d14109/pandas-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f8d68083e49e16b84734eb1a4dcae4259a75c90fb6e2251ab9a00b61120c06ab", size = 11409468, upload-time = "2026-03-31T06:46:45.2Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/46e7c76032639f2132359b5cf4c785dd8cf9aea5ea64699eac752f02b9db/pandas-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:32cc41f310ebd4a296d93515fcac312216adfedb1894e879303987b8f1e2b97d", size = 11936381, upload-time = "2026-03-31T06:46:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/7b/8b/721a9cff6fa6a91b162eb51019c6243b82b3226c71bb6c8ef4a9bd65cbc6/pandas-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:a4785e1d6547d8427c5208b748ae2efb64659a21bd82bf440d4262d02bfa02a4", size = 9744993, upload-time = "2026-03-31T06:46:51.488Z" }, + { url = "https://files.pythonhosted.org/packages/d5/18/7f0bd34ae27b28159aa80f2a6799f47fda34f7fb938a76e20c7b7fe3b200/pandas-3.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:08504503f7101300107ecdc8df73658e4347586db5cfdadabc1592e9d7e7a0fd", size = 9056118, upload-time = "2026-03-31T06:46:54.548Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ca/3e639a1ea6fcd0617ca4e8ca45f62a74de33a56ae6cd552735470b22c8d3/pandas-3.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b5918ba197c951dec132b0c5929a00c0bf05d5942f590d3c10a807f6e15a57d3", size = 10321105, upload-time = "2026-03-31T06:46:57.327Z" }, + { url = "https://files.pythonhosted.org/packages/0b/77/dbc82ff2fb0e63c6564356682bf201edff0ba16c98630d21a1fb312a8182/pandas-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d606a041c89c0a474a4702d532ab7e73a14fe35c8d427b972a625c8e46373668", size = 9864088, upload-time = "2026-03-31T06:46:59.935Z" }, + { url = "https://files.pythonhosted.org/packages/5c/2b/341f1b04bbca2e17e13cd3f08c215b70ef2c60c5356ef1e8c6857449edc7/pandas-3.0.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:710246ba0616e86891b58ab95f2495143bb2bc83ab6b06747c74216f583a6ac9", size = 10369066, upload-time = "2026-03-31T06:47:02.792Z" }, + { url = "https://files.pythonhosted.org/packages/12/c5/cbb1ffefb20a93d3f0e1fdcda699fb84976210d411b008f97f48bf6ce27e/pandas-3.0.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d3cfe227c725b1f3dff4278b43d8c784656a42a9325b63af6b1492a8232209e", size = 10876780, upload-time = "2026-03-31T06:47:06.205Z" }, + { url = "https://files.pythonhosted.org/packages/98/fe/2249ae5e0a69bd0ddf17353d0a5d26611d70970111f5b3600cdc8be883e7/pandas-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c3b723df9087a9a9a840e263ebd9f88b64a12075d1bf2ea401a5a42f254f084d", size = 11375181, upload-time = "2026-03-31T06:47:09.383Z" }, + { url = "https://files.pythonhosted.org/packages/de/64/77a38b09e70b6464883b8d7584ab543e748e42c1b5d337a2ee088e0df741/pandas-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a3096110bf9eac0070b7208465f2740e2d8a670d5cb6530b5bb884eca495fd39", size = 11928899, upload-time = "2026-03-31T06:47:12.686Z" }, + { url = "https://files.pythonhosted.org/packages/5e/52/42855bf626868413f761addd574acc6195880ae247a5346477a4361c3acb/pandas-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:07a10f5c36512eead51bc578eb3354ad17578b22c013d89a796ab5eee90cd991", size = 9746574, upload-time = "2026-03-31T06:47:15.64Z" }, + { url = "https://files.pythonhosted.org/packages/88/39/21304ae06a25e8bf9fc820d69b29b2c495b2ae580d1e143146c309941760/pandas-3.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:5fdbfa05931071aba28b408e59226186b01eb5e92bea2ab78b65863ca3228d84", size = 9047156, upload-time = "2026-03-31T06:47:18.595Z" }, + { url = "https://files.pythonhosted.org/packages/72/20/7defa8b27d4f330a903bb68eea33be07d839c5ea6bdda54174efcec0e1d2/pandas-3.0.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:dbc20dea3b9e27d0e66d74c42b2d0c1bed9c2ffe92adea33633e3bedeb5ac235", size = 10756238, upload-time = "2026-03-31T06:47:22.012Z" }, + { url = "https://files.pythonhosted.org/packages/e9/95/49433c14862c636afc0e9b2db83ff16b3ad92959364e52b2955e44c8e94c/pandas-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b75c347eff42497452116ce05ef461822d97ce5b9ff8df6edacb8076092c855d", size = 10408520, upload-time = "2026-03-31T06:47:25.197Z" }, + { url = "https://files.pythonhosted.org/packages/3b/f8/462ad2b5881d6b8ec8e5f7ed2ea1893faa02290d13870a1600fe72ad8efc/pandas-3.0.2-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1478075142e83a5571782ad007fb201ed074bdeac7ebcc8890c71442e96adf7", size = 10324154, upload-time = "2026-03-31T06:47:28.097Z" }, + { url = "https://files.pythonhosted.org/packages/0a/65/d1e69b649cbcddda23ad6e4c40ef935340f6f652a006e5cbc3555ac8adb3/pandas-3.0.2-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5880314e69e763d4c8b27937090de570f1fb8d027059a7ada3f7f8e98bdcb677", size = 10714449, upload-time = "2026-03-31T06:47:30.85Z" }, + { url = "https://files.pythonhosted.org/packages/47/a4/85b59bc65b8190ea3689882db6cdf32a5003c0ccd5a586c30fdcc3ffc4fc/pandas-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b5329e26898896f06035241a626d7c335daa479b9bbc82be7c2742d048e41172", size = 11338475, upload-time = "2026-03-31T06:47:34.026Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c4/bc6966c6e38e5d9478b935272d124d80a589511ed1612a5d21d36f664c68/pandas-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:81526c4afd31971f8b62671442a4b2b51e0aa9acc3819c9f0f12a28b6fcf85f1", size = 11786568, upload-time = "2026-03-31T06:47:36.941Z" }, + { url = "https://files.pythonhosted.org/packages/e8/74/09298ca9740beed1d3504e073d67e128aa07e5ca5ca2824b0c674c0b8676/pandas-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:7cadd7e9a44ec13b621aec60f9150e744cfc7a3dd32924a7e2f45edff31823b0", size = 10488652, upload-time = "2026-03-31T06:47:40.612Z" }, + { url = "https://files.pythonhosted.org/packages/bb/40/c6ea527147c73b24fc15c891c3fcffe9c019793119c5742b8784a062c7db/pandas-3.0.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:db0dbfd2a6cdf3770aa60464d50333d8f3d9165b2f2671bcc299b72de5a6677b", size = 10326084, upload-time = "2026-03-31T06:47:43.834Z" }, + { url = "https://files.pythonhosted.org/packages/95/25/bdb9326c3b5455f8d4d3549fce7abcf967259de146fe2cf7a82368141948/pandas-3.0.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0555c5882688a39317179ab4a0ed41d3ebc8812ab14c69364bbee8fb7a3f6288", size = 9914146, upload-time = "2026-03-31T06:47:46.67Z" }, + { url = "https://files.pythonhosted.org/packages/8d/77/3a227ff3337aa376c60d288e1d61c5d097131d0ac71f954d90a8f369e422/pandas-3.0.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:01f31a546acd5574ef77fe199bc90b55527c225c20ccda6601cf6b0fd5ed597c", size = 10444081, upload-time = "2026-03-31T06:47:49.681Z" }, + { url = "https://files.pythonhosted.org/packages/15/88/3cdd54fa279341afa10acf8d2b503556b1375245dccc9315659f795dd2e9/pandas-3.0.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:deeca1b5a931fdf0c2212c8a659ade6d3b1edc21f0914ce71ef24456ca7a6535", size = 10897535, upload-time = "2026-03-31T06:47:53.033Z" }, + { url = "https://files.pythonhosted.org/packages/06/9d/98cc7a7624f7932e40f434299260e2917b090a579d75937cb8a57b9d2de3/pandas-3.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f48afd9bb13300ffb5a3316973324c787054ba6665cda0da3fbd67f451995db", size = 11446992, upload-time = "2026-03-31T06:47:56.193Z" }, + { url = "https://files.pythonhosted.org/packages/9a/cd/19ff605cc3760e80602e6826ddef2824d8e7050ed80f2e11c4b079741dc3/pandas-3.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6c4d8458b97a35717b62469a4ea0e85abd5ed8687277f5ccfc67f8a5126f8c53", size = 11968257, upload-time = "2026-03-31T06:47:59.137Z" }, + { url = "https://files.pythonhosted.org/packages/db/60/aba6a38de456e7341285102bede27514795c1eaa353bc0e7638b6b785356/pandas-3.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:b35d14bb5d8285d9494fe93815a9e9307c0876e10f1e8e89ac5b88f728ec8dcf", size = 9865893, upload-time = "2026-03-31T06:48:02.038Z" }, + { url = "https://files.pythonhosted.org/packages/08/71/e5ec979dd2e8a093dacb8864598c0ff59a0cee0bbcdc0bfec16a51684d4f/pandas-3.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:63d141b56ef686f7f0d714cfb8de4e320475b86bf4b620aa0b7da89af8cbdbbb", size = 9188644, upload-time = "2026-03-31T06:48:05.045Z" }, + { url = "https://files.pythonhosted.org/packages/f1/6c/7b45d85db19cae1eb524f2418ceaa9d85965dcf7b764ed151386b7c540f0/pandas-3.0.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:140f0cffb1fa2524e874dde5b477d9defe10780d8e9e220d259b2c0874c89d9d", size = 10776246, upload-time = "2026-03-31T06:48:07.789Z" }, + { url = "https://files.pythonhosted.org/packages/a8/3e/7b00648b086c106e81766f25322b48aa8dfa95b55e621dbdf2fdd413a117/pandas-3.0.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae37e833ff4fed0ba352f6bdd8b73ba3ab3256a85e54edfd1ab51ae40cca0af8", size = 10424801, upload-time = "2026-03-31T06:48:10.897Z" }, + { url = "https://files.pythonhosted.org/packages/da/6e/558dd09a71b53b4008e7fc8a98ec6d447e9bfb63cdaeea10e5eb9b2dabe8/pandas-3.0.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d888a5c678a419a5bb41a2a93818e8ed9fd3172246555c0b37b7cc27027effd", size = 10345643, upload-time = "2026-03-31T06:48:13.7Z" }, + { url = "https://files.pythonhosted.org/packages/be/e3/921c93b4d9a280409451dc8d07b062b503bbec0531d2627e73a756e99a82/pandas-3.0.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b444dc64c079e84df91baa8bf613d58405645461cabca929d9178f2cd392398d", size = 10743641, upload-time = "2026-03-31T06:48:16.659Z" }, + { url = "https://files.pythonhosted.org/packages/56/ca/fd17286f24fa3b4d067965d8d5d7e14fe557dd4f979a0b068ac0deaf8228/pandas-3.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4544c7a54920de8eeacaa1466a6b7268ecfbc9bc64ab4dbb89c6bbe94d5e0660", size = 11361993, upload-time = "2026-03-31T06:48:19.475Z" }, + { url = "https://files.pythonhosted.org/packages/e4/a5/2f6ed612056819de445a433ca1f2821ac3dab7f150d569a59e9cc105de1d/pandas-3.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:734be7551687c00fbd760dc0522ed974f82ad230d4a10f54bf51b80d44a08702", size = 11815274, upload-time = "2026-03-31T06:48:22.695Z" }, + { url = "https://files.pythonhosted.org/packages/00/2f/b622683e99ec3ce00b0854bac9e80868592c5b051733f2cf3a868e5fea26/pandas-3.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:57a07209bebcbcf768d2d13c9b78b852f9a15978dac41b9e6421a81ad4cdd276", size = 10888530, upload-time = "2026-03-31T06:48:25.806Z" }, + { url = "https://files.pythonhosted.org/packages/cb/2b/f8434233fab2bd66a02ec014febe4e5adced20e2693e0e90a07d118ed30e/pandas-3.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:5371b72c2d4d415d08765f32d689217a43227484e81b2305b52076e328f6f482", size = 9455341, upload-time = "2026-03-31T06:48:28.418Z" }, +] + +[[package]] +name = "pillow" +version = "12.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, + { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" }, + { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" }, + { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" }, + { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" }, + { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, + { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, + { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, + { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, + { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, + { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, + { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, + { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, + { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, + { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, + { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, + { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, + { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, + { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, + { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, + { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, + { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, + { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, + { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, + { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, + { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, + { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, + { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, +] + +[[package]] +name = "pybind11" +version = "3.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/f0/35145a3c3baffeef55d4b8324caa33abaa8fa56ab345ecd4b2211d09163e/pybind11-3.0.4.tar.gz", hash = "sha256:3286b59c8a774b9ee650169302dd5a4eedc30a8617905a0560dd8ee44775130c", size = 589533, upload-time = "2026-04-19T03:08:15.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/06/c3a23c9a0263b136c519f033a58d4641e73065fefc7754e9667ec206d992/pybind11-3.0.4-py3-none-any.whl", hash = "sha256:961720ee652da51d531b7b2451a6bd2bc042b0106e6d9baa48ecb7d58034ce63", size = 314166, upload-time = "2026-04-19T03:08:14.091Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pynwb" +version = "3.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h5py" }, + { name = "hdmf" }, + { name = "numpy" }, + { name = "pandas" }, + { name = "platformdirs" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/83/0c37d99d2a262ca69be2df5bf9153b7bf2d79391d668e7ec232e89129ab4/pynwb-3.1.3.tar.gz", hash = "sha256:ee75219aa241b72b10d1c6830ae6bafa7e3e44f0429715c2ef0920a19654e185", size = 35784649, upload-time = "2025-12-09T23:40:50.358Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/47/b5b7d5285e823a16fe7b007bce3cf79ddcd3b6d7c8b3ca15cbe8c17c0a17/pynwb-3.1.3-py3-none-any.whl", hash = "sha256:43b5d753c6c2c8f6b4fa6c784ac85675556b64b42c727d9eab27670ef529929a", size = 1396673, upload-time = "2025-12-09T23:40:48.454Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "rich-click" +version = "1.9.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/27/091e140ea834272188e63f8dd6faac1f5c687582b687197b3e0ec3c78ebf/rich_click-1.9.7.tar.gz", hash = "sha256:022997c1e30731995bdbc8ec2f82819340d42543237f033a003c7b1f843fc5dc", size = 74838, upload-time = "2026-01-31T04:29:27.707Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/e5/d708d262b600a352abe01c2ae360d8ff75b0af819b78e9af293191d928e6/rich_click-1.9.7-py3-none-any.whl", hash = "sha256:2f99120fca78f536e07b114d3b60333bc4bb2a0969053b1250869bcdc1b5351b", size = 71491, upload-time = "2026-01-31T04:29:26.777Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, +] + +[[package]] +name = "ruamel-yaml" +version = "0.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/3b/ebda527b56beb90cb7652cb1c7e4f91f48649fbcd8d2eb2fb6e77cd3329b/ruamel_yaml-0.19.1.tar.gz", hash = "sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993", size = 142709, upload-time = "2026-01-02T16:50:31.84Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93", size = 118102, upload-time = "2026-01-02T16:50:29.201Z" }, +] + +[[package]] +name = "shapely" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/bc/0989043118a27cccb4e906a46b7565ce36ca7b57f5a18b78f4f1b0f72d9d/shapely-2.1.2.tar.gz", hash = "sha256:2ed4ecb28320a433db18a5bf029986aa8afcfd740745e78847e330d5d94922a9", size = 315489, upload-time = "2025-09-24T13:51:41.432Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/c0/f3b6453cf2dfa99adc0ba6675f9aaff9e526d2224cbd7ff9c1a879238693/shapely-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe2533caae6a91a543dec62e8360fe86ffcdc42a7c55f9dfd0128a977a896b94", size = 1833550, upload-time = "2025-09-24T13:50:30.019Z" }, + { url = "https://files.pythonhosted.org/packages/86/07/59dee0bc4b913b7ab59ab1086225baca5b8f19865e6101db9ebb7243e132/shapely-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ba4d1333cc0bc94381d6d4308d2e4e008e0bd128bdcff5573199742ee3634359", size = 1643556, upload-time = "2025-09-24T13:50:32.291Z" }, + { url = "https://files.pythonhosted.org/packages/26/29/a5397e75b435b9895cd53e165083faed5d12fd9626eadec15a83a2411f0f/shapely-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0bd308103340030feef6c111d3eb98d50dc13feea33affc8a6f9fa549e9458a3", size = 2988308, upload-time = "2025-09-24T13:50:33.862Z" }, + { url = "https://files.pythonhosted.org/packages/b9/37/e781683abac55dde9771e086b790e554811a71ed0b2b8a1e789b7430dd44/shapely-2.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e7d4d7ad262a48bb44277ca12c7c78cb1b0f56b32c10734ec9a1d30c0b0c54b", size = 3099844, upload-time = "2025-09-24T13:50:35.459Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f3/9876b64d4a5a321b9dc482c92bb6f061f2fa42131cba643c699f39317cb9/shapely-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e9eddfe513096a71896441a7c37db72da0687b34752c4e193577a145c71736fc", size = 3988842, upload-time = "2025-09-24T13:50:37.478Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a0/704c7292f7014c7e74ec84eddb7b109e1fbae74a16deae9c1504b1d15565/shapely-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:980c777c612514c0cf99bc8a9de6d286f5e186dcaf9091252fcd444e5638193d", size = 4152714, upload-time = "2025-09-24T13:50:39.9Z" }, + { url = "https://files.pythonhosted.org/packages/53/46/319c9dc788884ad0785242543cdffac0e6530e4d0deb6c4862bc4143dcf3/shapely-2.1.2-cp312-cp312-win32.whl", hash = "sha256:9111274b88e4d7b54a95218e243282709b330ef52b7b86bc6aaf4f805306f454", size = 1542745, upload-time = "2025-09-24T13:50:41.414Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bf/cb6c1c505cb31e818e900b9312d514f381fbfa5c4363edfce0fcc4f8c1a4/shapely-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:743044b4cfb34f9a67205cee9279feaf60ba7d02e69febc2afc609047cb49179", size = 1722861, upload-time = "2025-09-24T13:50:43.35Z" }, + { url = "https://files.pythonhosted.org/packages/c3/90/98ef257c23c46425dc4d1d31005ad7c8d649fe423a38b917db02c30f1f5a/shapely-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b510dda1a3672d6879beb319bc7c5fd302c6c354584690973c838f46ec3e0fa8", size = 1832644, upload-time = "2025-09-24T13:50:44.886Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ab/0bee5a830d209adcd3a01f2d4b70e587cdd9fd7380d5198c064091005af8/shapely-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8cff473e81017594d20ec55d86b54bc635544897e13a7cfc12e36909c5309a2a", size = 1642887, upload-time = "2025-09-24T13:50:46.735Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5e/7d7f54ba960c13302584c73704d8c4d15404a51024631adb60b126a4ae88/shapely-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe7b77dc63d707c09726b7908f575fc04ff1d1ad0f3fb92aec212396bc6cfe5e", size = 2970931, upload-time = "2025-09-24T13:50:48.374Z" }, + { url = "https://files.pythonhosted.org/packages/f2/a2/83fc37e2a58090e3d2ff79175a95493c664bcd0b653dd75cb9134645a4e5/shapely-2.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ed1a5bbfb386ee8332713bf7508bc24e32d24b74fc9a7b9f8529a55db9f4ee6", size = 3082855, upload-time = "2025-09-24T13:50:50.037Z" }, + { url = "https://files.pythonhosted.org/packages/44/2b/578faf235a5b09f16b5f02833c53822294d7f21b242f8e2d0cf03fb64321/shapely-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a84e0582858d841d54355246ddfcbd1fce3179f185da7470f41ce39d001ee1af", size = 3979960, upload-time = "2025-09-24T13:50:51.74Z" }, + { url = "https://files.pythonhosted.org/packages/4d/04/167f096386120f692cc4ca02f75a17b961858997a95e67a3cb6a7bbd6b53/shapely-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc3487447a43d42adcdf52d7ac73804f2312cbfa5d433a7d2c506dcab0033dfd", size = 4142851, upload-time = "2025-09-24T13:50:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/48/74/fb402c5a6235d1c65a97348b48cdedb75fb19eca2b1d66d04969fc1c6091/shapely-2.1.2-cp313-cp313-win32.whl", hash = "sha256:9c3a3c648aedc9f99c09263b39f2d8252f199cb3ac154fadc173283d7d111350", size = 1541890, upload-time = "2025-09-24T13:50:55.337Z" }, + { url = "https://files.pythonhosted.org/packages/41/47/3647fe7ad990af60ad98b889657a976042c9988c2807cf322a9d6685f462/shapely-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:ca2591bff6645c216695bdf1614fca9c82ea1144d4a7591a466fef64f28f0715", size = 1722151, upload-time = "2025-09-24T13:50:57.153Z" }, + { url = "https://files.pythonhosted.org/packages/3c/49/63953754faa51ffe7d8189bfbe9ca34def29f8c0e34c67cbe2a2795f269d/shapely-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2d93d23bdd2ed9dc157b46bc2f19b7da143ca8714464249bef6771c679d5ff40", size = 1834130, upload-time = "2025-09-24T13:50:58.49Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ee/dce001c1984052970ff60eb4727164892fb2d08052c575042a47f5a9e88f/shapely-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01d0d304b25634d60bd7cf291828119ab55a3bab87dc4af1e44b07fb225f188b", size = 1642802, upload-time = "2025-09-24T13:50:59.871Z" }, + { url = "https://files.pythonhosted.org/packages/da/e7/fc4e9a19929522877fa602f705706b96e78376afb7fad09cad5b9af1553c/shapely-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8d8382dd120d64b03698b7298b89611a6ea6f55ada9d39942838b79c9bc89801", size = 3018460, upload-time = "2025-09-24T13:51:02.08Z" }, + { url = "https://files.pythonhosted.org/packages/a1/18/7519a25db21847b525696883ddc8e6a0ecaa36159ea88e0fef11466384d0/shapely-2.1.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:19efa3611eef966e776183e338b2d7ea43569ae99ab34f8d17c2c054d3205cc0", size = 3095223, upload-time = "2025-09-24T13:51:04.472Z" }, + { url = "https://files.pythonhosted.org/packages/48/de/b59a620b1f3a129c3fecc2737104a0a7e04e79335bd3b0a1f1609744cf17/shapely-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:346ec0c1a0fcd32f57f00e4134d1200e14bf3f5ae12af87ba83ca275c502498c", size = 4030760, upload-time = "2025-09-24T13:51:06.455Z" }, + { url = "https://files.pythonhosted.org/packages/96/b3/c6655ee7232b417562bae192ae0d3ceaadb1cc0ffc2088a2ddf415456cc2/shapely-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6305993a35989391bd3476ee538a5c9a845861462327efe00dd11a5c8c709a99", size = 4170078, upload-time = "2025-09-24T13:51:08.584Z" }, + { url = "https://files.pythonhosted.org/packages/a0/8e/605c76808d73503c9333af8f6cbe7e1354d2d238bda5f88eea36bfe0f42a/shapely-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:c8876673449f3401f278c86eb33224c5764582f72b653a415d0e6672fde887bf", size = 1559178, upload-time = "2025-09-24T13:51:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/36/f7/d317eb232352a1f1444d11002d477e54514a4a6045536d49d0c59783c0da/shapely-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:4a44bc62a10d84c11a7a3d7c1c4fe857f7477c3506e24c9062da0db0ae0c449c", size = 1739756, upload-time = "2025-09-24T13:51:12.105Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c4/3ce4c2d9b6aabd27d26ec988f08cb877ba9e6e96086eff81bfea93e688c7/shapely-2.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:9a522f460d28e2bf4e12396240a5fc1518788b2fcd73535166d748399ef0c223", size = 1831290, upload-time = "2025-09-24T13:51:13.56Z" }, + { url = "https://files.pythonhosted.org/packages/17/b9/f6ab8918fc15429f79cb04afa9f9913546212d7fb5e5196132a2af46676b/shapely-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ff629e00818033b8d71139565527ced7d776c269a49bd78c9df84e8f852190c", size = 1641463, upload-time = "2025-09-24T13:51:14.972Z" }, + { url = "https://files.pythonhosted.org/packages/a5/57/91d59ae525ca641e7ac5551c04c9503aee6f29b92b392f31790fcb1a4358/shapely-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f67b34271dedc3c653eba4e3d7111aa421d5be9b4c4c7d38d30907f796cb30df", size = 2970145, upload-time = "2025-09-24T13:51:16.961Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cb/4948be52ee1da6927831ab59e10d4c29baa2a714f599f1f0d1bc747f5777/shapely-2.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:21952dc00df38a2c28375659b07a3979d22641aeb104751e769c3ee825aadecf", size = 3073806, upload-time = "2025-09-24T13:51:18.712Z" }, + { url = "https://files.pythonhosted.org/packages/03/83/f768a54af775eb41ef2e7bec8a0a0dbe7d2431c3e78c0a8bdba7ab17e446/shapely-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1f2f33f486777456586948e333a56ae21f35ae273be99255a191f5c1fa302eb4", size = 3980803, upload-time = "2025-09-24T13:51:20.37Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/559c7c195807c91c79d38a1f6901384a2878a76fbdf3f1048893a9b7534d/shapely-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cf831a13e0d5a7eb519e96f58ec26e049b1fad411fc6fc23b162a7ce04d9cffc", size = 4133301, upload-time = "2025-09-24T13:51:21.887Z" }, + { url = "https://files.pythonhosted.org/packages/80/cd/60d5ae203241c53ef3abd2ef27c6800e21afd6c94e39db5315ea0cbafb4a/shapely-2.1.2-cp314-cp314-win32.whl", hash = "sha256:61edcd8d0d17dd99075d320a1dd39c0cb9616f7572f10ef91b4b5b00c4aeb566", size = 1583247, upload-time = "2025-09-24T13:51:23.401Z" }, + { url = "https://files.pythonhosted.org/packages/74/d4/135684f342e909330e50d31d441ace06bf83c7dc0777e11043f99167b123/shapely-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:a444e7afccdb0999e203b976adb37ea633725333e5b119ad40b1ca291ecf311c", size = 1773019, upload-time = "2025-09-24T13:51:24.873Z" }, + { url = "https://files.pythonhosted.org/packages/a3/05/a44f3f9f695fa3ada22786dc9da33c933da1cbc4bfe876fe3a100bafe263/shapely-2.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5ebe3f84c6112ad3d4632b1fd2290665aa75d4cef5f6c5d77c4c95b324527c6a", size = 1834137, upload-time = "2025-09-24T13:51:26.665Z" }, + { url = "https://files.pythonhosted.org/packages/52/7e/4d57db45bf314573427b0a70dfca15d912d108e6023f623947fa69f39b72/shapely-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5860eb9f00a1d49ebb14e881f5caf6c2cf472c7fd38bd7f253bbd34f934eb076", size = 1642884, upload-time = "2025-09-24T13:51:28.029Z" }, + { url = "https://files.pythonhosted.org/packages/5a/27/4e29c0a55d6d14ad7422bf86995d7ff3f54af0eba59617eb95caf84b9680/shapely-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b705c99c76695702656327b819c9660768ec33f5ce01fa32b2af62b56ba400a1", size = 3018320, upload-time = "2025-09-24T13:51:29.903Z" }, + { url = "https://files.pythonhosted.org/packages/9f/bb/992e6a3c463f4d29d4cd6ab8963b75b1b1040199edbd72beada4af46bde5/shapely-2.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a1fd0ea855b2cf7c9cddaf25543e914dd75af9de08785f20ca3085f2c9ca60b0", size = 3094931, upload-time = "2025-09-24T13:51:32.699Z" }, + { url = "https://files.pythonhosted.org/packages/9c/16/82e65e21070e473f0ed6451224ed9fa0be85033d17e0c6e7213a12f59d12/shapely-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:df90e2db118c3671a0754f38e36802db75fe0920d211a27481daf50a711fdf26", size = 4030406, upload-time = "2025-09-24T13:51:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/7c/75/c24ed871c576d7e2b64b04b1fe3d075157f6eb54e59670d3f5ffb36e25c7/shapely-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:361b6d45030b4ac64ddd0a26046906c8202eb60d0f9f53085f5179f1d23021a0", size = 4169511, upload-time = "2025-09-24T13:51:36.297Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f7/b3d1d6d18ebf55236eec1c681ce5e665742aab3c0b7b232720a7d43df7b6/shapely-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:b54df60f1fbdecc8ebc2c5b11870461a6417b3d617f555e5033f1505d36e5735", size = 1602607, upload-time = "2025-09-24T13:51:37.757Z" }, + { url = "https://files.pythonhosted.org/packages/9a/f6/f09272a71976dfc138129b8faf435d064a811ae2f708cb147dccdf7aacdb/shapely-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:0036ac886e0923417932c2e6369b6c52e38e0ff5d9120b90eef5cd9a5fc5cae9", size = 1796682, upload-time = "2025-09-24T13:51:39.233Z" }, +] + +[[package]] +name = "simplejson" +version = "4.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0e/2a/54837395a3487c725669428d513293612a48d82b95a0642c936932e5d898/simplejson-4.1.1.tar.gz", hash = "sha256:c08eb9f7a90f77ae470e19a07472e9a79ebc0d1c2315d86a72767665bd5ba79f", size = 118860, upload-time = "2026-04-24T19:24:59.819Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/25/e90998fe8e480eb43b966c09e835379887d427567ebd496563d3b1e16b19/simplejson-4.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:19040a17154dc03d289bab68d73ce0a6a0be01de30c584bbdd93490bead14b22", size = 112414, upload-time = "2026-04-24T19:23:06.084Z" }, + { url = "https://files.pythonhosted.org/packages/9c/a0/abd4785f36c3400f1fbb21f517be39295a750a714f04b7ee175adf6ef580/simplejson-4.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a94ebaecdbaa80d9551a3ec6bf0c9302fc8b53ab6c1b2bfd498a1df4cb28158d", size = 91120, upload-time = "2026-04-24T19:23:07.877Z" }, + { url = "https://files.pythonhosted.org/packages/b8/78/fc060d2e3b13c6ec59288574b8efac64075e316b2afba4396a56b2422f78/simplejson-4.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:67341c95c0a168ab4a6d1e807e50463f1c8da932c3286d81e201266c427061fa", size = 91055, upload-time = "2026-04-24T19:23:09.264Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b6/156a8de1e1b47694f0e7de6675866936608d45dc68388fd017d36f8693be/simplejson-4.1.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:45ec18e337fec538b7e902d489505c450b2454653d1290f3f50385e6fd8aa607", size = 190297, upload-time = "2026-04-24T19:23:11.226Z" }, + { url = "https://files.pythonhosted.org/packages/86/1c/e4d0eab695be3eb21d0f46bce820752031f03e7113f9c80a9b3c73ee7157/simplejson-4.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:820c69a4710400e9b248d5670647d60be58824369282d3925e516b3ff1a7cd82", size = 187002, upload-time = "2026-04-24T19:23:12.982Z" }, + { url = "https://files.pythonhosted.org/packages/76/0e/7f5a59d29426b062d5928fb88b403c3f797129d53be7102f955dbe51aa44/simplejson-4.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e708d373a10e4378ef2d59f8361850c7150fd907ed49efe49bc5492160476d1", size = 195146, upload-time = "2026-04-24T19:23:14.517Z" }, + { url = "https://files.pythonhosted.org/packages/78/18/9943db224dd4d5fa3c090c3e56a94c37b254338c83995ec5680285111c40/simplejson-4.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:980fc33353f81fd12d8c49d44f8c2760d1dc8192285e627c5180d141035b228a", size = 183931, upload-time = "2026-04-24T19:23:16.742Z" }, + { url = "https://files.pythonhosted.org/packages/c2/08/9a690da9a766161c06c627d805362cf159f1abe480969372b2897649b955/simplejson-4.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:de2ed102fff88dacf543699f53ee3a533cc11539a39baa176b7e09dd783069d6", size = 192228, upload-time = "2026-04-24T19:23:18.33Z" }, + { url = "https://files.pythonhosted.org/packages/05/88/bd8aad36b451ffb0e0a3f721d695a88befa6d1ac7d1e02ae788ca7ff4029/simplejson-4.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2785ff8edc0e28bf773a32543a6bbed46351453c997b3f6709c744e3c2f7eabb", size = 187808, upload-time = "2026-04-24T19:23:21.165Z" }, + { url = "https://files.pythonhosted.org/packages/04/ee/14f91db0d1f481533b651dafbf8cd0da088d9817f7af30c68f7f19f9c847/simplejson-4.1.1-cp312-cp312-win32.whl", hash = "sha256:2e0d5ead6d14610467ec356ec1f6b5d8a56aa216abaad8d41c8b873b16cf313f", size = 88512, upload-time = "2026-04-24T19:23:22.764Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c4/90de06b2d8737c68c05ff9274113f854dbf6a5f28b7a955212111672cb57/simplejson-4.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:63a5451f557d6be48a231bae932458655c620902b868170b2f1c8afed496f6b4", size = 90748, upload-time = "2026-04-24T19:23:24.494Z" }, + { url = "https://files.pythonhosted.org/packages/37/a9/47b445eeb559c9593453a0648e0fd6d08e8adff64dd5e5ced66726da8a09/simplejson-4.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dff52fc7af272e84fc21cc5a06c927c823ca6ae00af14f3b0d7707b42775ed98", size = 113160, upload-time = "2026-04-24T19:23:26.033Z" }, + { url = "https://files.pythonhosted.org/packages/4c/65/cb72db31523c164dea5dc55b02dad065a40c478856bc7534b279d2b51906/simplejson-4.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:971aed0647ad6e840a3943bec812fcda5f2d26a5497a4981d1fb49aa4f9a396c", size = 91521, upload-time = "2026-04-24T19:23:27.572Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e5/54cb7c50ad5fdc1e0a86b7df4b135c2cbd5c4623605aa94466659098e8da/simplejson-4.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:249e2e220aa6d9b9d936bde84eb7bf79d5b6c5a8273c6e411f8b1635a9073f2d", size = 91407, upload-time = "2026-04-24T19:23:28.991Z" }, + { url = "https://files.pythonhosted.org/packages/38/2e/21a3ede87f0bf82d6c7bcb90480d50a6490eb974c6ab20881188e440957c/simplejson-4.1.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e5cdd6a5d52299f345c15ab5678cc4249e24f383f361d986afbc3c7072a6b6b", size = 192451, upload-time = "2026-04-24T19:23:30.56Z" }, + { url = "https://files.pythonhosted.org/packages/59/df/9903edd3102bf0b5984edfcb90c88612330996efa3b4fbf8a971d6e17839/simplejson-4.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:642cec364e0676e2d5a73fa4d31d0c7c55886997caa2fde24e8292ca44d32728", size = 189015, upload-time = "2026-04-24T19:23:32.647Z" }, + { url = "https://files.pythonhosted.org/packages/98/cd/33230927a780e1398b857e3944abb914556994d252b1d765ae40d112cb25/simplejson-4.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:76fe296ca1df23d290033f10aaacf534fd1b3e3007e7f9ff8aa68b21413aaa78", size = 196658, upload-time = "2026-04-24T19:23:34.563Z" }, + { url = "https://files.pythonhosted.org/packages/cd/84/2c5a7444eb53e9a86d3738299bffddd9f53aeed799ded2f45368221fdb19/simplejson-4.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f0ad25b7dc4e0fb23858355819f2e994f1a5badcdcde8737eac7921c2f1ed2a", size = 185967, upload-time = "2026-04-24T19:23:36.191Z" }, + { url = "https://files.pythonhosted.org/packages/d3/68/454378e06d059cd412a7ed5d87fb6d29fd5b60f13a4d89fc1f764ff434df/simplejson-4.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a59ebd0533f03fd06ff0c42ba0f02d93cbcdd7944922bf3b93911327a95b901f", size = 193940, upload-time = "2026-04-24T19:23:38.151Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d5/a15bf915f623a2c5a079d6e3be8256fdb8ef06f110669493a09b9d6933e0/simplejson-4.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bccbf4419676b517939852e5aeff2af6aee4dc046881c67a1581fa6f1cb01abd", size = 189795, upload-time = "2026-04-24T19:23:40.139Z" }, + { url = "https://files.pythonhosted.org/packages/d2/c9/37212ae7dc4b607f0978c408e8633f05c810884e054c33113184c6c2c8a2/simplejson-4.1.1-cp313-cp313-win32.whl", hash = "sha256:6c845363eb5fd166fb7c72243da38f4fcfde666ede7fdf2cc6fd7762894626f7", size = 88773, upload-time = "2026-04-24T19:23:41.754Z" }, + { url = "https://files.pythonhosted.org/packages/fe/a5/c7a0a47883a9015b54c9d8a4b62f2aba17bd4335b1787b9b8a0fc2fa6d52/simplejson-4.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:104d8324c34f25b4b90800bc5fa363780cbc3d8496aef061cba7ce1af9162270", size = 90888, upload-time = "2026-04-24T19:23:43.11Z" }, + { url = "https://files.pythonhosted.org/packages/d3/18/4a118a6a92eb33bb08c8e2fe7ec85cb96f0673491bb2b829930831ee4fbe/simplejson-4.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:ed7473602b6625de793b6acba49aa949f144a475f538792067e4cf2fda2071f5", size = 110492, upload-time = "2026-04-24T19:23:44.957Z" }, + { url = "https://files.pythonhosted.org/packages/07/f4/84d160e9fa8cada1e0a9381cae4fa81eecd573577a5b34366d8ced59bdf7/simplejson-4.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:225c9caa324c5b554d009fb9cac22aee7711e71bd96f487938c659af467e828e", size = 90152, upload-time = "2026-04-24T19:23:46.355Z" }, + { url = "https://files.pythonhosted.org/packages/68/31/9a5432c433a7671107182cdc9a20ea78a70f99c4e5334aa54b6d4d0d79ed/simplejson-4.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:95407269340c7f22f09776ea7b717a52cf56cfcf119b5e45f66faa4a26445bea", size = 90115, upload-time = "2026-04-24T19:23:47.743Z" }, + { url = "https://files.pythonhosted.org/packages/78/91/3635cdb13318cb0a328abaa69e2b91251caad39d6779aa308098f341f6cb/simplejson-4.1.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3851658d642c1184d2023f0e6c9ce44a21eb1629e74e7c84ef956b128841fe12", size = 184036, upload-time = "2026-04-24T19:23:49.472Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/149b6ec5393f6849d98c59cadba888b710a8ef4b805ab91e11a566960d40/simplejson-4.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:95a3bb0f78e85f4937f99092239f2011ce06f0f2d803df5c299cc05abbeae008", size = 180543, upload-time = "2026-04-24T19:23:51.023Z" }, + { url = "https://files.pythonhosted.org/packages/df/7c/a5d968d0b527a748b667e62bea94309ccbcb1e2b108e8f0cf8547efaa12b/simplejson-4.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bbfdaa7c0603f75b7b14b211b7f2be44696d4e26833ad2d91d5c87bf5fb9a920", size = 188725, upload-time = "2026-04-24T19:23:52.995Z" }, + { url = "https://files.pythonhosted.org/packages/db/e3/6a8d11181d587ef00e2db9112357e6832111e56dd56b01b5c11758a1965d/simplejson-4.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:39e3c584071dced8c21b4689f0254303521daeb9b5bc1f4289755d71fa3cb0d3", size = 177492, upload-time = "2026-04-24T19:23:54.581Z" }, + { url = "https://files.pythonhosted.org/packages/67/e3/8b0eb8b06e8198cfbd1270487da163d0093df05cc4f557350cd65e2f7e79/simplejson-4.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:036a27bd0469b9d79557cbddb392969f876cd7f278cfbd0fba81534927a06575", size = 185281, upload-time = "2026-04-24T19:23:56.13Z" }, + { url = "https://files.pythonhosted.org/packages/dc/5f/64990f07ec9e2cb1a814c674e2e21b5693207f74ac70eb72151b847ea4e6/simplejson-4.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b70bfd2f67f3351baba08aa3ae9233c83f21fd95ae5e6b3d0ecb8c647929112f", size = 181848, upload-time = "2026-04-24T19:23:57.92Z" }, + { url = "https://files.pythonhosted.org/packages/61/a5/bbc1bc0447f339f79f99ab8c37f7f037cb2f1f93af75d6a4d553096bb0c3/simplejson-4.1.1-cp314-cp314-win32.whl", hash = "sha256:37233c72ce88d06acb92747347742b3c07871eba6789f060c179c9302dde8efe", size = 88761, upload-time = "2026-04-24T19:23:59.397Z" }, + { url = "https://files.pythonhosted.org/packages/18/72/ec1b5cbdcb140c132e6c7bdf99bd73e4f675439e77126c88f472fcffa09c/simplejson-4.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:cc0442dea71cd9cbf30a0b8b9929ab5aa6c02c0443a3d977351e6ec5bada4388", size = 91018, upload-time = "2026-04-24T19:24:00.85Z" }, + { url = "https://files.pythonhosted.org/packages/3d/97/4fa437f68ff72219bac3bf3d050de9c6265691f3a170e16954bd69d7cddd/simplejson-4.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:c996a4d38290c515af347740659ce095b425449c164a5c9fa3977caa6eff5dbe", size = 113919, upload-time = "2026-04-24T19:24:02.287Z" }, + { url = "https://files.pythonhosted.org/packages/c2/83/59de041d09eb4a9577f7015d7263c32095dfb7fde49717dff62145d89809/simplejson-4.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c65c763fb20d7ca113c1c14dce2fc04a0fc3a57aceff533d6fdac707c7bffb40", size = 91904, upload-time = "2026-04-24T19:24:03.812Z" }, + { url = "https://files.pythonhosted.org/packages/03/8e/46bb345d540f6eb31427d984a4e518cdb182d0621814fee4fee045e8815b/simplejson-4.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0da5c9f57206ee7ef280ff7f1d924937b0a64f9a271a5ef371a2ecdbebba7421", size = 91752, upload-time = "2026-04-24T19:24:05.622Z" }, + { url = "https://files.pythonhosted.org/packages/83/e2/1b2ce97f068835eb3d253c116a4df7a3f436b7bf2fb5ff1ba29287e8b0ec/simplejson-4.1.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ea3426e786425d10e9e82f8a6eda74a7d6eb10d99165ac3d0d3bbcb65c0ea343", size = 214021, upload-time = "2026-04-24T19:24:07.447Z" }, + { url = "https://files.pythonhosted.org/packages/48/70/d93e556df6a0786298644a7c08304fcbeddc248325f23f38acbebeb21165/simplejson-4.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d75cea7a1025edd7e439b2966b3d977c45b5b899e2adaf422811b3ac702ed9fb", size = 213530, upload-time = "2026-04-24T19:24:09.289Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a5/c93bf305b9f00d7259e09e713d60e75bd0f7f53da970f716ab90491770e7/simplejson-4.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:63c2ada8e58f266491f19eed2eeeb7c25c6141e52f8f9e820f6bb94156cf8dbc", size = 218282, upload-time = "2026-04-24T19:24:10.991Z" }, + { url = "https://files.pythonhosted.org/packages/0c/20/a9b5d2e27ec44b069ee251bd55544fc76929a067107b1050001566ba86f3/simplejson-4.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d1fffb56305c5b475ee746cf9e04f97423ba5aaacd292dc1255bd75b1d3b124b", size = 209249, upload-time = "2026-04-24T19:24:12.662Z" }, + { url = "https://files.pythonhosted.org/packages/97/e4/e06ee682ed5df67592181f5ecb062e35878967e27f5b6e087237d4548d95/simplejson-4.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a6525ec733f43d0541206cffa64fd2aad5a7ae3eb76566aff49cd4db6382209a", size = 213963, upload-time = "2026-04-24T19:24:14.302Z" }, + { url = "https://files.pythonhosted.org/packages/9c/9f/1e160e4cd8cdbf062bf6a454cdf814dc7a48eb47e566fdb8f80ccb202605/simplejson-4.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:861e393260508efa64d8805a8e49c416c3484907e3f146ce966c69552b49b9a3", size = 210474, upload-time = "2026-04-24T19:24:15.917Z" }, + { url = "https://files.pythonhosted.org/packages/7a/e6/cecd913df322df5bbe7ebb8ba39e0708e505a165553900da8a7761026d6f/simplejson-4.1.1-cp314-cp314t-win32.whl", hash = "sha256:d083b89d30948a751d3d97476c2ed91e4caaa24a1a1459bdbadb8876242c71fe", size = 91134, upload-time = "2026-04-24T19:24:17.635Z" }, + { url = "https://files.pythonhosted.org/packages/97/73/f540dde99cc1d393bd062ab3b5735b777561a5d8f8a5f2e241164444d77a/simplejson-4.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4cbb299d0528ec0447fe366d8c9641860e28f997a62730690fef905f1f41046e", size = 94467, upload-time = "2026-04-24T19:24:19.109Z" }, + { url = "https://files.pythonhosted.org/packages/ce/6a/8b74c52ffd33dbbde00fe7251fee6a0acdc8cea33f7a43805aed258fb79b/simplejson-4.1.1-py3-none-any.whl", hash = "sha256:2ce92b3748f02423e26d2bfb636fb9d7a8f67c8f5854dcae69d350d123b2eee2", size = 69195, upload-time = "2026-04-24T19:24:57.962Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "skia-python" +version = "144.0.post2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pybind11" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/e8/3e8316416a302d38a5e95560383c5a1769f59f943b3685fdceb4ab42445a/skia_python-144.0.post2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d8bf9a203448a98324c108bbbfa3a9f94ff8514212ae832a81d2b60a5a7dbbfe", size = 12090279, upload-time = "2026-03-19T22:21:20.216Z" }, + { url = "https://files.pythonhosted.org/packages/75/76/b434c18948051ce4a71760115bbdfe81ebba690834fcd830a9e14c9955af/skia_python-144.0.post2-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:a4df01d2fc7733748f81acf2d0ace313f6b400d92e5e599d7eb2386a205c501c", size = 12196828, upload-time = "2026-03-19T22:21:22.472Z" }, + { url = "https://files.pythonhosted.org/packages/14/fa/f60ace54c44ce622a3727df2df5a3a16a07046daa74e2a52bc8bb6755ab5/skia_python-144.0.post2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:2dc7a62b4e7f9e03c77ac7daefaa96184e505038a99a7461499519642a5297fa", size = 13925370, upload-time = "2026-03-19T22:21:24.722Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d0/c223526c203fbebcf401c7ddff3736aa03223c22d140671ddb535805d382/skia_python-144.0.post2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:970404599aa6f6dc19e2ae36ab0a0aabdf14410d1e1974f5deabae9fc9c1b193", size = 14388831, upload-time = "2026-03-19T22:21:26.952Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e9/fdd00636f4efc89b4daa28a7e7a2e2d52fc4e6e1b1f93c790eaceeb94b81/skia_python-144.0.post2-cp312-cp312-win_amd64.whl", hash = "sha256:2268d85e19dd22181fb780cfc59bfe1df33c9cad9ba0df4bf15ff8ee8b6430e5", size = 10883210, upload-time = "2026-03-19T22:21:29.286Z" }, + { url = "https://files.pythonhosted.org/packages/da/35/78c97fcba0bfc0cfcb078baaed416780ce8fe45132ebb279903d3025c490/skia_python-144.0.post2-cp312-cp312-win_arm64.whl", hash = "sha256:b3cd83eab87def5475a46a19c130f595f69877bedc291fcc84db2af562480782", size = 10470024, upload-time = "2026-03-19T22:21:31.417Z" }, + { url = "https://files.pythonhosted.org/packages/5c/b9/52ee5d2acd637d448f4276778ae2395c784da86163583ea1086afe02b620/skia_python-144.0.post2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ebdaa28e20c5200a974d12d9ad5fa70597512c171431cfb1cec04b3c2ead9db4", size = 12089934, upload-time = "2026-03-19T22:21:33.556Z" }, + { url = "https://files.pythonhosted.org/packages/79/1d/dcf61033e46ec7748eb7a3bd25b727a01b627981d9a743f5bf1e68d04f42/skia_python-144.0.post2-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:9bdcbd8272755a03b2dd85ae7e56cdc0e1f95618a1e985bc8a87ad5ad5be47ce", size = 12197351, upload-time = "2026-03-19T22:21:35.866Z" }, + { url = "https://files.pythonhosted.org/packages/cd/be/e42aa62b9f9c94b9c9ed34f5cffc85cbd9cbc8d08327c8499ccc19a3e7d2/skia_python-144.0.post2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:559bd25feec895c0af214caf311ffec098004438c668183a9277eab518d08c3f", size = 13925039, upload-time = "2026-03-19T22:21:38.041Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ea/6d5db07d00d8381a686efff218324c767b31834d0b90dac6c1039afb15ea/skia_python-144.0.post2-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:0922676ec89fc88b04fe891edf59166b32f905979dbd4489608bc4bd8d7d12d4", size = 14389390, upload-time = "2026-03-19T22:21:40.67Z" }, + { url = "https://files.pythonhosted.org/packages/40/6e/0bcbec5e32d30e55396f02f19e5342e9d039996b99438b5ce4d3d8d89c2e/skia_python-144.0.post2-cp313-cp313-win_amd64.whl", hash = "sha256:eb2a31e2eee1f8f8626d20cff7fc4655495b4a05e30e8803454e4f7de4ef89eb", size = 10883475, upload-time = "2026-03-19T22:21:43.393Z" }, + { url = "https://files.pythonhosted.org/packages/7a/3b/0e2e2ed23888f82f3ecd1243ab98eb58cadc7706678d5a173d95d7f77efc/skia_python-144.0.post2-cp313-cp313-win_arm64.whl", hash = "sha256:146fdd31ff0c030c18b9f17dc8e65a58f8821ed774151ec1499d9f0587d63ca5", size = 10469471, upload-time = "2026-03-19T22:21:45.966Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ee/5521e4f3c21e0a3629d7c889f75ccdc5083e6a92ff6eeab1494a14bd88f2/skia_python-144.0.post2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:904020d6e1aa144e0d1cede5e25b5285d01183ab05e714a6143ac64792772c5b", size = 12025810, upload-time = "2026-03-19T22:21:47.959Z" }, + { url = "https://files.pythonhosted.org/packages/77/18/e83f7a7e93ed6f24617fcbd8c867dc6746b800e3cdf84f0a735ca015ef2f/skia_python-144.0.post2-cp314-cp314-macosx_11_0_x86_64.whl", hash = "sha256:23a736a877d65e4199680ca2a5a58d8df2e0e4362793d8b03a5d90536f39ccf8", size = 12205269, upload-time = "2026-03-19T22:21:49.975Z" }, + { url = "https://files.pythonhosted.org/packages/7d/34/31cfc614c84b2dea5f4c25e5ee834e1375002d331a564926a11d90bb489e/skia_python-144.0.post2-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:f2520b34106d3d0d275191c8913ab861372b8bced689cedebbee2df2ff49905c", size = 13937050, upload-time = "2026-03-19T22:21:52.226Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c9/6dc194a1387002da0b0492ed75ea61cdf94b177b7fc49e0d8f00e72335a2/skia_python-144.0.post2-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:2d97991ecd1fc22606e7e6aa4ab1f1951c9c9b7409f39da346e1788eabc44d7e", size = 14391905, upload-time = "2026-03-19T22:21:54.406Z" }, + { url = "https://files.pythonhosted.org/packages/c8/cf/1b9775332ef12d9778057d9547a41c82d9684b1152d857edb3a22580700a/skia_python-144.0.post2-cp314-cp314-win_amd64.whl", hash = "sha256:44df4ccc65ea8c288a43694ec25cdbd13e18013b48425ff822ad2de09c168a4d", size = 11260774, upload-time = "2026-03-19T22:21:57.327Z" }, + { url = "https://files.pythonhosted.org/packages/67/da/fcf8d9dc87740c5b2e2588721717f64d5abc666993b992223b16f34161ba/skia_python-144.0.post2-cp314-cp314-win_arm64.whl", hash = "sha256:de848e3f384a70251b02fad5a31f64c9ef72866d0cbd3728ef97aa355ee01a1d", size = 10840101, upload-time = "2026-03-19T22:21:59.489Z" }, + { url = "https://files.pythonhosted.org/packages/cd/05/449b2bdbae3835c30585857975a684c709110b29ff49c4021dc4dce2da46/skia_python-144.0.post2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c24a6b67c912ca49582bf66d73e325ec0a4085e70e4c659c72146fa1d3be48be", size = 12289885, upload-time = "2026-03-19T22:22:01.415Z" }, + { url = "https://files.pythonhosted.org/packages/3f/26/0d42a3992bea0c8b0cde4047ac05c03b87d50777826d277931fe5341ce16/skia_python-144.0.post2-cp314-cp314t-macosx_11_0_x86_64.whl", hash = "sha256:08523720c5bd3b36ba0e36dee14f4a7911c23256a527ad3b73e1521813bc649d", size = 12348560, upload-time = "2026-03-19T22:22:03.68Z" }, + { url = "https://files.pythonhosted.org/packages/56/ab/f7e1f107f99db0fb88f621a1607f5ae65cd9e81cffcdd2246d76dfad3ec5/skia_python-144.0.post2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76281944d4db84b9e9968224462ccc5714810d3e29e5555486c73be515408602", size = 13929098, upload-time = "2026-03-19T22:22:06.02Z" }, + { url = "https://files.pythonhosted.org/packages/30/63/966d2237d113155adce1f2c53acb21e136826086aa1acdb9d30d1245eeb0/skia_python-144.0.post2-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:416cf18cfba129967cc21aeb7eaa934c413e5dc977f753e5e1ffa27546d21e14", size = 14391812, upload-time = "2026-03-19T22:22:08.382Z" }, + { url = "https://files.pythonhosted.org/packages/72/0d/c89d170436881da8a36df78e37a99d73c72b47ac4331a37a5a4e1ac919b7/skia_python-144.0.post2-cp314-cp314t-win_amd64.whl", hash = "sha256:df7fc6d3b713debf8b099d7a1e9888d855ea898f9f02bac3bf0ed73c0a184473", size = 11531971, upload-time = "2026-03-19T22:22:10.564Z" }, + { url = "https://files.pythonhosted.org/packages/08/5d/97ad2d5379feb5cda6b4040b6b8f5a3bd18d01eaad1bb24478fea2bf704b/skia_python-144.0.post2-cp314-cp314t-win_arm64.whl", hash = "sha256:214644901fa5ad77edd82ca3b2c4fb6106196d0e323d5cae982cc5fe0a05b6a6", size = 10931677, upload-time = "2026-03-19T22:22:12.806Z" }, +] + +[[package]] +name = "sleap-io" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "colorcet" }, + { name = "h5py" }, + { name = "imageio" }, + { name = "imageio-ffmpeg" }, + { name = "lazy-loader" }, + { name = "ndx-multisubjects" }, + { name = "ndx-pose" }, + { name = "numpy" }, + { name = "pandas" }, + { name = "pynwb" }, + { name = "pyyaml" }, + { name = "rich-click" }, + { name = "shapely" }, + { name = "simplejson" }, + { name = "skia-python" }, + { name = "tifffile" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/41/de163cf758096846551bb4e01477d5c03451b764bc4d5f2f15d5b9d20c97/sleap_io-0.7.0.tar.gz", hash = "sha256:74acb1337b2a11afe199c1dc6a88658f0bfe77d46ce7006f70978602c6fc2ad1", size = 774716, upload-time = "2026-05-04T17:43:22.598Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/3f/5dd786ad3e92f51c1d886b4541664e18d912f0f9b4bbbeac73aac76eddd2/sleap_io-0.7.0-py3-none-any.whl", hash = "sha256:ac91a087663cdb44582ef4433d0028a1b3a2b6dcf3fe3bfa9cfab64bf3762031", size = 832857, upload-time = "2026-05-04T17:43:21.077Z" }, +] + +[[package]] +name = "tifffile" +version = "2026.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6c/3e/695c7ab56be57814e369c1f38bc3f64b9dea0a83e867d00c0c9d613a9929/tifffile-2026.5.2.tar.gz", hash = "sha256:21b10227ede8493814a34676774797f721f487e36cb0530e7c3bd882caa87f5a", size = 429140, upload-time = "2026-05-02T20:19:31.497Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/af/ce4df3ca29122d219c45d3e86e5ff9a9df03b8cf31afd76817b662c803a3/tifffile-2026.5.2-py3-none-any.whl", hash = "sha256:5129b53b826e768a5b1af26b765eeea75c2d0a227d2d12849617e0737588e105", size = 266420, upload-time = "2026-05-02T20:19:29.814Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "tzdata" +version = "2026.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, +]