Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 3 additions & 23 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,28 +1,8 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.egg-info/
.eggs/
build/
.pytest_cache/
.coverage
dist/

# Virtual environments
build/
.venv/
venv/
env/

# Testing / coverage
.coverage
.coverage.*
htmlcov/
.pytest_cache/
coverage.xml

# IDE
.vscode/
.idea/
*.swp

# OS
.DS_Store
39 changes: 29 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
[![Tests](https://github.com/sequana/webapp_samplesheet/actions/workflows/tests.yml/badge.svg)](https://github.com/sequana/webapp_samplesheet/actions/workflows/tests.yml)
[![Release](https://img.shields.io/github/v/release/sequana/webapp_samplesheet)](https://github.com/sequana/webapp_samplesheet/releases)
[![License](https://img.shields.io/badge/license-BSD--3--Clause-blue.svg)](LICENSE)
![Python](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12-blue.svg)
![Python](https://img.shields.io/badge/python-3.9%20%7C%203.10%20%7C%203.11%20%7C%203.12-blue.svg)
[![Streamlit App](https://img.shields.io/badge/Streamlit-Live%20Demo-FF4B4B?logo=streamlit&logoColor=white)](https://check-my-sample-sheet.streamlit.app/)
![Visitors](https://api.visitorbadge.io/api/visitors?path=https%3A%2F%2Fcheck-my-sample-sheet.streamlit.app%2F&countColor=%23263759)

This is a streamlit application that uses Sequana (github.com/sequana/sequana) **iem** modules to check Sample Sheet from Illumina sequencers.
This is a streamlit application that uses Sequana (github.com/sequana/sequana) **iem** modules to check Sample Sheets from Illumina sequencers. Both formats are supported and detected automatically:

- **v1** (bcl2fastq): `[Data]` / `[Settings]` sections
- **v2** (BCL Convert): `[BCLConvert_Data]` / `[BCLConvert_Settings]` sections

Running demo is here: https://check-my-sample-sheet.streamlit.app/

Expand All @@ -16,17 +19,33 @@ Running demo is here: https://check-my-sample-sheet.streamlit.app/

If you want to contribute to this web application, please provide PR here. Note, however, that the core of the application is within the Sequana project on https://github.com/sequana/sequana/, more specifically in the iem.py module.

The sanity checks implemented are based on experience and the bcl2fastq documentation v2.20
The sanity checks implemented are based on experience, the bcl2fastq documentation (v2.20) and the BCL Convert specification.

# Installation

From PyPI:

pip install check-my-sample-sheet

Then launch the app (opens in your browser); extra arguments are forwarded to
`streamlit run` (e.g. `--server.port 8502`):

check-my-sample-sheet

# Local instance (from source)

git clone https://github.com/sequana/webapp_samplesheet
cd webapp_samplesheet

# Local instance
# install the dependencies (sequana, streamlit, ...)
pip install -r requirements.txt

git clone https://github.com/sequana/webapp_samplesheet check_my_sample_sheet
cd check_my_sample_sheet
# run the application locally in your browser
streamlit run check_my_sample_sheet/app.py

# You will need to install requirements (sequana and streamlit)
pip install --file requirements.txt
# Running the tests

# and should ne ready to test the appliction locally in your browser
streamlit run app.py
pip install -r requirements-dev.txt
pytest


1 change: 1 addition & 0 deletions check_my_sample_sheet/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__version__ = "1.0.0"
19 changes: 19 additions & 0 deletions check_my_sample_sheet/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""Console entry point: launch the Streamlit sample sheet validator.

Installed as the ``check-my-sample-sheet`` command (see pyproject.toml). Any
extra arguments are forwarded to ``streamlit run`` (e.g. ``--server.port``).
"""
import sys
from pathlib import Path

from streamlit.web import cli as stcli


def main():
app = str(Path(__file__).resolve().parent / "app.py")
sys.argv = ["streamlit", "run", app, *sys.argv[1:]]
sys.exit(stcli.main())


if __name__ == "__main__":
main()
53 changes: 40 additions & 13 deletions app.py → check_my_sample_sheet/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,17 @@

import requests
import streamlit as st
from sequana.iem import SampleSheet
from sequana.iem import SampleSheetFactory, get_sample_sheet_version
from streamlit_option_menu import option_menu

# directory holding this module, used to resolve packaged assets (imgs, examples)
# regardless of the current working directory.
HERE = Path(__file__).resolve().parent
LOGO = str(HERE / "imgs" / "logo_256x256.png")

st.set_page_config(
page_title="Illumina Sample Sheet Validator",
page_icon="imgs/logo_256x256.png",
page_title="Check My Sample Sheet",
page_icon=LOGO,
layout="wide",
menu_items={"Report a bug": "https://github.com/sequana/webapp_samplesheet/issues/new/choose"},
)
Expand Down Expand Up @@ -125,6 +130,11 @@ def add_legend(success, warning, error):
if "code_input" not in st.session_state:
st.session_state.code_input = ""

# used to reset the file_uploader when an example is loaded: bumping this counter
# changes the widget key, which forces Streamlit to drop any previously uploaded file.
if "uploader_key" not in st.session_state:
st.session_state.uploader_key = 0


def load_example(filename):
"""Load example file from examples directory."""
Expand All @@ -134,14 +144,19 @@ def load_example(filename):


def set_example(filename):
"""Callback to load example into the textarea session state."""
"""Callback to load example into the textarea session state.

Also resets the file_uploader (by bumping its key) so a previously uploaded
file does not silently take precedence over the loaded example.
"""
st.session_state.code_input = load_example(filename)
st.session_state.uploader_key += 1


def main():
st.sidebar.write("Provided by the [Sequana team](https://github.com/sequana/sequana)")
st.sidebar.image("imgs/logo_256x256.png")
st.title(f"Sample Sheet and Design Validator (v{version})")
st.sidebar.image(LOGO)
st.title(f"Check My Sample Sheet (v{version})")

menu = ["Sample Sheet Validation (Illumina)", "Examples", "About", "How to cite"]

Expand All @@ -160,7 +175,8 @@ def main():
if choice == "Sample Sheet Validation (Illumina)":

st.markdown(
"This tool validates Illumina sample sheets against the bcl2fastq v2.20 specification. "
"This tool validates Illumina sample sheets. Both the **v1** format (bcl2fastq v2.20) and the "
"**v2** format (BCL Convert) are supported; the version is detected automatically. "
"It checks the structure, mandatory sections, sample identifiers, indexes, and more. "
"Provide a sample sheet below for validation, or load one of the examples to try the tool. "
"More examples are available in the **Examples** section of the menu."
Expand All @@ -171,7 +187,9 @@ def main():
col1, col2, col3 = st.columns([4, 1, 4])
with col1:
data_file = st.file_uploader(
"Drop a sample sheet below and press the **Process** button. ", type=["csv", "txt"]
"Drop a sample sheet below and press the **Process** button. ",
type=["csv", "txt"],
key=f"uploader_{st.session_state.uploader_key}",
)
with col2:
# Centered "OR" text
Expand All @@ -185,15 +203,18 @@ def main():
st.subheader("Load an Example", divider="blue")
st.caption(
"Click a button to load a sample sheet into the text area above. "
"Examples 1 and 2 are valid sheets; Example 3 is invalid and demonstrates how errors are reported."
"Examples 1, 2 and 4 are valid sheets; Example 3 is invalid and demonstrates how errors are reported. "
"Example 4 is a v2 (BCL Convert) sheet, the others are v1 (bcl2fastq)."
)
example_col1, example_col2, example_col3 = st.columns(3)
example_col1, example_col2, example_col3, example_col4 = st.columns(4)
with example_col1:
st.button("Example 1: Dual indexing", on_click=set_example, args=("sample_sheet.csv",))
st.button("Example 1: Dual indexing (v1)", on_click=set_example, args=("sample_sheet.csv",))
with example_col2:
st.button("Example 2: Single index + Settings", on_click=set_example, args=("sample_sheet_settings_index.csv",))
st.button("Example 2: Single index + Settings (v1)", on_click=set_example, args=("sample_sheet_settings_index.csv",))
with example_col3:
st.button("Example 3: Invalid (bad sample ID)", on_click=set_example, args=("Bad_SampleSheet_alphanum.csv",))
with example_col4:
st.button("Example 4: BCL Convert (v2)", on_click=set_example, args=("sample_sheet_v2_bclconvert.csv",))

if st.button(":gear: Process :gear:"):

Expand Down Expand Up @@ -322,7 +343,13 @@ def process_sample_sheet(data_file, samplesheet):
with tempfile.NamedTemporaryFile(delete=False, mode="w") as fout:
fout.write(samplesheet)
fout.close()
iem = SampleSheet(fout.name)
version = get_sample_sheet_version(fout.name)
iem = SampleSheetFactory(fout.name)

if version == "v2":
st.info(":information_source: Detected an Illumina **v2** sample sheet: validating against the **BCL Convert** specification.")
else:
st.info(":information_source: Detected an Illumina **v1** sample sheet: validating against the **bcl2fastq v2.20** specification.")

try:
# st.write(f"This sample sheet contains {len(iem.df)} samples")
Expand Down
4 changes: 4 additions & 0 deletions check_my_sample_sheet/examples/case1.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[Data]
Sample_ID,index
ID1,TGACCA
ID2,CATTTT
7 changes: 7 additions & 0 deletions check_my_sample_sheet/examples/case2.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[Settings]
Adapter, ACGTACGTN

[Data]
Sample_ID,index
ID1,TGACCa
ID1,CATTTT
8 changes: 8 additions & 0 deletions check_my_sample_sheet/examples/case3.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[Settings];;;;
Adapter;AGATCGGAAGAGCACACGTCTGAACTCCAGTCA;;;
AdapterRead2;AGATCGGAAGAGCGTCGTGTAGGGAAAGAGTGT;;;
;;;
[Data];;;
Sample_ID;Sample_Name;I7_Index_ID;index;Sample_Project
A;;NF01;CGATGT;
B;;NF03;ACAGTG;
File renamed without changes.
44 changes: 44 additions & 0 deletions check_my_sample_sheet/examples/sample_sheet_v2_bclconvert.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
[Header],
FileFormatVersion,2
RunName,20260616_B21184-18S-PE-MPdbl-151-10-5M
InstrumentPlatform,MiSeqi100Series
IndexOrientation,Forward
AnalysisLocation,Local

[Reads]
Read1Cycles,151
Read2Cycles,151
Index1Cycles,10
Index2Cycles,10

[Sequencing_Settings]
LibraryPrepKits,NexteraXT

[BCLConvert_Settings]
SoftwareVersion,4.4.6
AdapterRead1,CTGTCTCTTATACACATCT
AdapterRead2,CTGTCTCTTATACACATCT
OverrideCycles,R1:Y151;I1:I10;I2:I10;R2:Y151
FastqCompressionFormat,dragen
NoLaneSplitting,true
GenerateFastqcMetrics,true

[BCLConvert_Data]
Sample_ID,Index,Index2
1,AGGTCAGATA,CTACAAGATA
2,CGACATCCGA,TACGTTCATT
3,ATTCCATAAG,TGCCTGGTGG
4,CACAATAGGA,TCCATCCGAG
5,AACATCGCGC,GTCCACTTGT


[Cloud_Settings]
GeneratedVersion,1.25.0.202605080250

[Cloud_Data]
Sample_ID,ProjectName,LibraryName,LibraryPrepKitName,IndexAdapterKitName
1,BXXXX,1_AGGTCAGATA_CTACAAGATA,NexteraXT,IlluminaDNARNAUDISetABCDTagmentation
2,BXXXX,2_CGACATCCGA_TACGTTCATT,NexteraXT,IlluminaDNARNAUDISetABCDTagmentation
3,BXXXX,3_ATTCCATAAG_TGCCTGGTGG,NexteraXT,IlluminaDNARNAUDISetABCDTagmentation
4,BXXXX,4_CACAATAGGA_TCCATCCGAG,NexteraXT,IlluminaDNARNAUDISetABCDTagmentation
5,BXXXX,5_AACATCGCGC_GTCCACTTGT,NexteraXT,IlluminaDNARNAUDISetABCDTagmentation
File renamed without changes
42 changes: 41 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,43 @@
[tool.poetry]
name = "check-my-sample-sheet"
version = "1.0.0"
description = "Streamlit web application to validate Illumina sample sheets (bcl2fastq v1 and BCL Convert v2)."
authors = ["Thomas Cokelaer <cokelaer@gmail.com>"]
license = "BSD-3-Clause"
readme = "README.md"
homepage = "https://github.com/sequana/webapp_samplesheet"
repository = "https://github.com/sequana/webapp_samplesheet"
documentation = "https://github.com/sequana/webapp_samplesheet"
keywords = ["illumina", "samplesheet", "bcl2fastq", "bcl-convert", "sequana", "ngs"]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Science/Research",
"License :: OSI Approved :: BSD License",
"Programming Language :: Python :: 3",
"Topic :: Scientific/Engineering :: Bio-Informatics",
]
packages = [{ include = "check_my_sample_sheet" }]
include = ["check_my_sample_sheet/examples/*", "check_my_sample_sheet/imgs/*"]

[tool.poetry.dependencies]
python = ">=3.9,<4.0"
# sequana >= 0.23.0 ships BCLConvert / SampleSheetFactory / get_sample_sheet_version
sequana = ">=0.23.0"
streamlit = ">=1.28"
streamlit-option-menu = "*"
requests = "*"

[tool.poetry.group.dev.dependencies]
pytest = "*"
pytest-cov = "*"

[tool.poetry.scripts]
check-my-sample-sheet = "check_my_sample_sheet.__main__:main"

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

[tool.pytest.ini_options]
addopts = "--cov=app --cov-report=term-missing"
addopts = "--cov=check_my_sample_sheet --cov-report=term-missing"
testpaths = ["tests"]
5 changes: 3 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
sequana==0.21.2
# sequana >= 0.23.0 ships BCLConvert / SampleSheetFactory / get_sample_sheet_version
sequana>=0.23.0
streamlit>=1.28
streamlit_option_menu
setuptools
requests
5 changes: 3 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,20 @@
import pytest

PROJECT_ROOT = Path(__file__).parent.parent
PACKAGE_DIR = PROJECT_ROOT / "check_my_sample_sheet"
sys.path.insert(0, str(PROJECT_ROOT))


@pytest.fixture
def app_path():
"""Path to the Streamlit app entrypoint."""
return str(PROJECT_ROOT / "app.py")
return str(PACKAGE_DIR / "app.py")


@pytest.fixture
def examples_dir():
"""Path to the examples directory."""
return PROJECT_ROOT / "examples"
return PACKAGE_DIR / "examples"


@pytest.fixture
Expand Down
2 changes: 1 addition & 1 deletion tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def test_default_menu_is_validation(app_path):
at = AppTest.from_file(app_path, default_timeout=APP_TIMEOUT)
at.run()
# Title rendered on every page
assert any("Sample Sheet and Design Validator" in t.value for t in at.title)
assert any("Check My Sample Sheet" in t.value for t in at.title)
# Validation page has the Input subheader
subheaders = [s.value for s in at.subheader]
assert "Input Sample Sheet" in subheaders
Expand Down
2 changes: 1 addition & 1 deletion tests/test_helpers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Unit tests for helper functions in app.py."""
import pytest

from app import load_example
from check_my_sample_sheet.app import load_example


def test_load_example_valid_returns_content():
Expand Down
Loading