From fa1818a81df20951c8715a5a7d20a47afdec5576 Mon Sep 17 00:00:00 2001 From: Sarah Griffiths Date: Thu, 7 Jul 2022 19:14:52 +0000 Subject: [PATCH] template updates --- .github/ISSUE_TEMPLATE/bug_report.yml | 76 +++++++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 5 ++ CHANGELOG.md | 5 ++ lib/ArgumentParser.groovy | 61 ++++++++++++++++++++ lib/fastqingress.nf | 82 ++++++++++++++++++++------- main.nf | 9 ++- nextflow.config | 5 +- nextflow_schema.json | 3 +- 8 files changed, 219 insertions(+), 27 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 lib/ArgumentParser.groovy diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..a3dac6d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,76 @@ +name: Bug Report +description: File a bug report +title: "[Bug]: " +labels: ["bug", "triage"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + - type: textarea + id: what-happened + attributes: + label: What happened? + description: Also tell us, what did you expect to happen? + placeholder: Tell us what you see! + value: "A bug happened!" + validations: + required: true + - type: dropdown + id: os + attributes: + label: Operating System + description: What operating system are you running? + options: + - Windows 10 + - Windows 11 + - macOS + - ubuntu 18.04 + - ubuntu 20.04 + validations: + required: true + - type: dropdown + id: execution + attributes: + label: Workflow Execution + description: Where are you running the workflow? + options: + - EPI2ME Labs desktop application + - Command line + - EPI2ME + - Other (please describe) + validations: + required: true + - type: input + id: labs-version + attributes: + label: Workflow Execution - EPI2ME Labs Versions + description: If you're running using EPI2ME Labs please provide the version and the environment version (Click the blue help icon bottom left and select ":bout")? + validations: + required: false + - type: dropdown + id: profile + attributes: + label: Workflow Execution - Execution Profile + description: If you're using the CLI to run the workflow, what profile are you using? + options: + - Docker + - Singularity + - Conda + validations: + required: false + - type: input + id: version + attributes: + label: Workflow Version + description: What version of the workflow are you running? + validations: + required: true + - type: textarea + id: logs + attributes: + label: Relevant log output + description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. + render: shell + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..868c05e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Nanopore customer support + url: https://nanoporetech.com/contact + about: For general support, including bioinformatics questions. diff --git a/CHANGELOG.md b/CHANGELOG.md index 29ac54d..5e72e16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [v0.1.4] +### Changed +- Args parser for fastqingress +- Set out_dir option type to ensure output is written to correct directory on Windows + ## [v0.1.3] ### Changed - Better help text on cli diff --git a/lib/ArgumentParser.groovy b/lib/ArgumentParser.groovy new file mode 100644 index 0000000..3ce7377 --- /dev/null +++ b/lib/ArgumentParser.groovy @@ -0,0 +1,61 @@ +/* Check arguments of a Nextflow function + * + * Nextflow script does not support the Groovy idiom: + * + * def function(Map args[:], arg1, arg2, ...) + * + * to support unordered kwargs. The methods here are designed + * to reduce boileplate while allowing Nextflow script to implement + * + * def function(Map args[:]) + * + * with required and default values. This is similar to some Python + * libraries' (notably matplotlib) extensive use of things like: + * + * def function(*args, **kwargs) + * + * to implement generic APIs. Why do we want to do all this? Because + * we want to write library code with a clean set of required parameters + * but also extensible with non-required parameters with default values. + * This allows us to later add parameters without breaking existing code, + * and is very common practice elsewhere. + */ + +import java.util.Set + +class ArgumentParser { + Set args + Map kwargs + String name + + /* Parse arguments, raising an error on unknown keys */ + public Map parse_args(LinkedHashMap given_args) { + Set opt_keys = kwargs.keySet() + Set given_keys = given_args.keySet() + check_required(given_keys) + check_unknown(given_keys, opt_keys) + return kwargs + given_args + } + + /* Parse arguments, without raising an error for extra keys */ + public Map parse_known_args(LinkedHashMap given_args) { + Set opt_keys = kwargs.keySet() + Set given_keys = given_args.keySet() + check_required(given_keys) + return kwargs + given_args + } + + private void check_required(Set given) { + Set missing_keys = args - given + if (!missing_keys.isEmpty()) { + throw new Exception("Missing arguments for function ${name}: ${missing_keys}") + } + } + + private void check_unknown(Set given, Set kwargs_keys) { + Set extra_keys = given - (args + kwargs_keys) + if (!extra_keys.isEmpty()) { + throw new Exception("Unknown arguments provided to function ${name}: ${extra_keys}.") + } + } +} diff --git a/lib/fastqingress.nf b/lib/fastqingress.nf index 2ac43f6..2782e02 100644 --- a/lib/fastqingress.nf +++ b/lib/fastqingress.nf @@ -1,5 +1,7 @@ +import ArgumentParser + process handleSingleFile { - label "isoforms" + label params.process_label cpus 1 input: file reads @@ -16,7 +18,7 @@ process handleSingleFile { process checkSampleSheet { - label "isoforms" + label params.process_label cpus 1 input: file "sample_sheet.txt" @@ -73,7 +75,7 @@ def find_fastq(pattern, maxdepth) * @param input_folder Top-level input directory. * @param staging Top-level output_directory. * @return A File object representating the staging directory created - * under output_folder + * under output */ def sanitize_fastq(input_folder, staging) { @@ -111,8 +113,8 @@ def sanitize_fastq(input_folder, staging) */ def get_subdirectories(input_directory) { - barcode_dirs = file("$input_directory/barcode*", type: 'dir', maxdepth: 1) - all_dirs = file("$input_directory/*", type: 'dir', maxdepth: 1) + barcode_dirs = file(input_directory.resolve("barcode*"), type: 'dir', maxdepth: 1) + all_dirs = file(input_directory.resolve("*"), type: 'dir', maxdepth: 1) non_barcoded = ( all_dirs + barcode_dirs ) - all_dirs.intersect(barcode_dirs) return [barcode_dirs, non_barcoded] } @@ -223,9 +225,11 @@ def handle_flat_dir(input_directory, sample_name) * @param barcoded_dirs List of barcoded directories (barcodeXX,...) * @param sample_sheet List of tuples mapping barcode to sample name * or a simple string for non-multiplexed data. + * @param min_barcode Minimum barcode to accept. + * @param max_barcode Maximum (inclusive) barcode to accept. * @return Channel of tuples (path, sample_id, type) */ -def handle_barcoded_dirs(barcoded_dirs, sample_sheet) +def handle_barcoded_dirs(barcoded_dirs, sample_sheet, min_barcode, max_barcode) { valid_dirs = get_valid_directories(barcoded_dirs) // link sample names to barcode through sample sheet @@ -233,17 +237,36 @@ def handle_barcoded_dirs(barcoded_dirs, sample_sheet) sample_sheet = Channel .fromPath(valid_dirs) .filter(~/.*barcode[0-9]{1,3}$/) // up to 192 + .filter { barcode_in_range(it, min_barcode, max_barcode) } .map { path -> tuple(path.baseName, path.baseName, 'test_sample') } } return Channel .fromPath(valid_dirs) .filter(~/.*barcode[0-9]{1,3}$/) // up to 192 + .filter { barcode_in_range(it, min_barcode, max_barcode) } .map { path -> tuple(path.baseName, path) } .join(sample_sheet) .map { barcode, path, sample, type -> tuple(path, sample, type) } } +/** + * Determine if a barcode path is within a required numeric range + * + * @param path barcoded directory (barcodeXX). + * @param min_barcode Minimum barcode to accept. + * @param max_barcode Maximum (inclusive) barcode to accept. + */ +def barcode_in_range(path, min_barcode, max_barcode) +{ + pattern = ~/barcode(\d+)/ + matcher = "${path}" =~ pattern + value = matcher[0][1].toInteger() + valid = ((value >= min_barcode) && (value <= max_barcode)) + return valid +} + + /** * Take a list of non-barcode directories to return a channel * of named samples. Samples are named by directory baseName. @@ -264,33 +287,49 @@ def handle_non_barcoded_dirs(non_barcoded_dirs) * Take an input (file or directory) and return a channel of * named samples. * - * * @param input Top level input file or folder to locate fastq data. * @param sample string to name single sample data. * @param sample_sheet Path to sample sheet CSV file. + * @param sanitize regularize inputs from EPI2ME platform. + * @param output output location, required if sanitize==true + * @param min_barcode Minimum barcode to accept. + * @param max_barcode Maximum (inclusive) barcode to accept. + * * @return Channel of tuples (path, sample_id, type) */ -def fastq_ingress(input, output_folder, sample, sample_sheet, sanitize) +def fastq_ingress(Map arguments) { + def parser = new ArgumentParser( + args:["input"], + kwargs:[ + "sample":null, "sample_sheet":null, "sanitize":false, "output":null, + "min_barcode":0, "max_barcode":Integer.MAX_VALUE], + name:"fastq_ingress") + Map margs = parser.parse_args(arguments) + + if (margs.sanitize && margs.output == null) { + throw new Exception("Argument 'output' required if 'sanitize' is true.") + } + println("Checking fastq input.") - input = file(input); + input = file(margs.input) // Handle file input if (input.isFile()) { // Assume sample is a string at this point println('Single file input detected.') - if (sample_sheet) { + if (margs.sample_sheet) { println('Warning: `--sample_sheet` given but single file input found. Ignoring.') } - return handle_single_file(input, sample) + return handle_single_file(input, margs.sample) } // Handle directory input if (input.isDirectory()) { // EPI2ME harness - if (sanitize) { - staging = file(output_folder).resolve("staging") - input = sanitize_fastq(file(input), staging) + if (margs.sanitize) { + staging = file(margs.output).resolve("staging") + input = sanitize_fastq(input, staging) } // Get barcoded and non barcoded subdirectories @@ -299,13 +338,13 @@ def fastq_ingress(input, output_folder, sample, sample_sheet, sanitize) // Case 03: If no subdirectories, handle the single dir if (!barcoded && !non_barcoded) { println("Single directory input detected.") - if (sample_sheet) { + if (margs.sample_sheet) { println('Warning: `--sample_sheet` given but single non-barcode directory found. Ignoring.') } - return handle_flat_dir(input, sample) + return handle_flat_dir(input, margs.sample) } - if (sample) { + if (margs.sample) { println('Warning: `--sample` given but multiple directories found, ignoring.') } @@ -314,16 +353,17 @@ def fastq_ingress(input, output_folder, sample, sample_sheet, sanitize) barcoded_samples = Channel.empty() if (barcoded) { println("Barcoded directories detected.") - if (sample_sheet) { - sample_sheet = get_sample_sheet(sample_sheet) + sample_sheet = null + if (margs.sample_sheet) { + sample_sheet = get_sample_sheet(margs.sample_sheet) } - barcoded_samples = handle_barcoded_dirs(barcoded, sample_sheet) + barcoded_samples = handle_barcoded_dirs(barcoded, sample_sheet, margs.min_barcode, margs.max_barcode) } non_barcoded_samples = Channel.empty() if (non_barcoded) { println("Non barcoded directories detected.") - if (!barcoded && sample_sheet) { + if (!barcoded && margs.sample_sheet) { println('Warning: `--sample_sheet` given but no barcode directories found.') } non_barcoded_samples = handle_non_barcoded_dirs(non_barcoded) diff --git a/main.nf b/main.nf index 0f892ae..942d8d6 100644 --- a/main.nf +++ b/main.nf @@ -502,9 +502,12 @@ workflow { ref_annotation = file("$projectDir/data/OPTIONAL_FILE") } - reads = fastq_ingress( - params.fastq, params.out_dir, params.sample, params.sample_sheet, params.sanitize_fastq - ) + reads = fastq_ingress([ + "input":params.fastq, + "sample":params.sample, + "sample_sheet":params.sample_sheet, + "sanitize": params.sanitize_fastq, + "output":params.out_dir]) pipeline(reads, ref_genome, ref_annotation) diff --git a/nextflow.config b/nextflow.config index a6f3263..ba5ef0b 100644 --- a/nextflow.config +++ b/nextflow.config @@ -23,15 +23,16 @@ params { sample = null sample_sheet = null sanitize_fastq = false - wfversion = "v0.1.3" + wfversion = "v0.1.4" aws_image_prefix = null aws_queue = null report_name = "report" + process_label = "isoforms" monochrome_logs = false validate_params = true show_hidden_params = false - schema_ignore_params = 'show_hidden_params,validate_params,monochrome_logs,aws_queue,aws_image_prefix,wfversion,wf' + schema_ignore_params = 'show_hidden_params,validate_params,monochrome_logs,aws_queue,aws_image_prefix,wfversion,wf,process_label' // Process cDNA reads using pychopper, turn off for direct RNA: direct_rna = false diff --git a/nextflow_schema.json b/nextflow_schema.json index 1b69ba1..272d98a 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -15,6 +15,7 @@ "out_dir": { "type": "string", "default": "output", + "format": "path", "description": "Directory for output of all user-facing files." }, "fastq": { @@ -280,7 +281,7 @@ }, "wfversion": { "type": "string", - "default": "v0.1.3", + "default": "v0.1.4", "hidden": true }, "monochrome_logs": {