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
40 changes: 40 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: StaphSCAN test

on:
push:
branches: [ "main", "master" ]
pull_request:
branches: [ "main", "master" ]

jobs:
test:
runs-on: ubuntu-latest

strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip

- name: Install Mash and blast
run: |
sudo apt-get update
sudo apt-get install -y mash ncbi-blast+

- name: Install dependencies and staphscan
run: |
python -m pip install --upgrade pip
pip install pytest pandas
pip install .

- name: Run full pipeline test
run: |
pytest test/ -v
92 changes: 92 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Changelog

## [Unreleased](https://github.com/riccabolla/staphscan/tree/HEAD)

[Full Changelog](https://github.com/riccabolla/staphscan/compare/v0.4.0...HEAD)

**Implemented enhancements:**

- v0.4.0 release [\#13](https://github.com/riccabolla/StaphSCAN/issues/13)

## [v0.4.0](https://github.com/riccabolla/staphscan/tree/v0.4.0) (2026-06-26)

[Full Changelog](https://github.com/riccabolla/staphscan/compare/v0.3.1...v0.4.0)

**Fixed bugs:**

- error in containers built from Bioconda [\#11](https://github.com/riccabolla/StaphSCAN/issues/11)

**Merged pull requests:**

- V0.4.0 [\#14](https://github.com/riccabolla/StaphSCAN/pull/14) ([riccabolla](https://github.com/riccabolla))

## [v0.3.1](https://github.com/riccabolla/staphscan/tree/v0.3.1) (2026-05-02)

[Full Changelog](https://github.com/riccabolla/staphscan/compare/v0.3.0...v0.3.1)

**Implemented enhancements:**

- mlst downgrade [\#10](https://github.com/riccabolla/StaphSCAN/issues/10)

**Merged pull requests:**

- V0.3.1 [\#12](https://github.com/riccabolla/StaphSCAN/pull/12) ([riccabolla](https://github.com/riccabolla))

## [v0.3.0](https://github.com/riccabolla/staphscan/tree/v0.3.0) (2026-03-23)

[Full Changelog](https://github.com/riccabolla/staphscan/compare/v0.2.1...v0.3.0)

**Closed issues:**

- Broken DOI badge [\#9](https://github.com/riccabolla/StaphSCAN/issues/9)

## [v0.2.1](https://github.com/riccabolla/staphscan/tree/v0.2.1) (2026-03-06)

[Full Changelog](https://github.com/riccabolla/staphscan/compare/v0.2.0...v0.2.1)

**Fixed bugs:**

- Error running resistance: sequence has zero length [\#8](https://github.com/riccabolla/StaphSCAN/issues/8)

## [v0.2.0](https://github.com/riccabolla/staphscan/tree/v0.2.0) (2026-02-26)

[Full Changelog](https://github.com/riccabolla/staphscan/compare/v0.1.3...v0.2.0)

**Fixed bugs:**

- sccmec subtype [\#7](https://github.com/riccabolla/StaphSCAN/issues/7)

## [v0.1.3](https://github.com/riccabolla/staphscan/tree/v0.1.3) (2026-02-16)

[Full Changelog](https://github.com/riccabolla/staphscan/compare/v0.1.2...v0.1.3)

## [v0.1.2](https://github.com/riccabolla/staphscan/tree/v0.1.2) (2026-02-08)

[Full Changelog](https://github.com/riccabolla/staphscan/compare/v0.1.1...v0.1.2)

## [v0.1.1](https://github.com/riccabolla/staphscan/tree/v0.1.1) (2026-02-03)

[Full Changelog](https://github.com/riccabolla/staphscan/compare/v0.1.0...v0.1.1)

**Closed issues:**

- First release issues [\#2](https://github.com/riccabolla/StaphSCAN/issues/2)

## [v0.1.0](https://github.com/riccabolla/staphscan/tree/v0.1.0) (2026-02-02)

[Full Changelog](https://github.com/riccabolla/staphscan/compare/d72545e6a8e370300f26ccf4d23eb2a329cd199d...v0.1.0)

**Implemented enhancements:**

- Res db [\#5](https://github.com/riccabolla/StaphSCAN/issues/5)
- mlst [\#4](https://github.com/riccabolla/StaphSCAN/issues/4)
- biofilm submodule [\#3](https://github.com/riccabolla/StaphSCAN/issues/3)
- Output [\#1](https://github.com/riccabolla/StaphSCAN/issues/1)

**Closed issues:**

- Complete report [\#6](https://github.com/riccabolla/StaphSCAN/issues/6)



\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# StaphSCAN
[![CI](https://github.com/riccabolla/staphscan/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/riccabolla/staphscan/actions/workflows/ci.yml)
[![Anaconda-Server Badge](https://anaconda.org/bioconda/staphscan/badges/version.svg)](https://anaconda.org/bioconda/staphscan)
[![Anaconda-Server Badge](https://anaconda.org/bioconda/staphscan/badges/latest_release_date.svg)](https://anaconda.org/bioconda/staphscan)
[![Anaconda-Server Badge](https://anaconda.org/bioconda/staphscan/badges/license.svg)](https://anaconda.org/bioconda/staphscan)
Expand Down
4 changes: 4 additions & 0 deletions docs/assembly.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ To report the species, the following criteria are used:
For assembly quality the following parameters are considered:

* Total assembly size (compared to expected size for *Staphylococcus aureus* of 2.6 - 3.1 Mbp)
* Contamination: If more than one reference genome returns a Mash distance of **≤ 0.04**, the assembly is flagged (`Mixed`). This indicates an intra-genus mixed culture (e.g., *S. aureus* + *S. epidermidis*).
* N50 (>=10 kbp)
* Presence of ambiguous bases (Ns)

Expand All @@ -48,7 +49,10 @@ The assembly module generates the following output columns in the report:
|Field|Description|
|-----|-----------|
|`Species`|Detected specie|
|`Mash_distance`|Calculated Mash distance to the best-matching reference genome|
|`Total_size`|Assembly size (bp)|
|`N_contig`|Total number of contigs in the assembly|
|`N50`|Assembly N50 value|
|`QC`|Overall QC status (`PASS` or `FAILED`)|


Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

setup(
name="staphscan",
version="0.4.0",
version="0.4.1",
description="A tool for Staphylococcus aureus analysis",
author="Riccardo Bollini",
url="https://github.com/riccabolla/StaphSCAN",
Expand Down
4 changes: 3 additions & 1 deletion staphscan/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,9 @@ def main():
df = pd.DataFrame(all_results).fillna("-")

summary_cols = [
"Sample", "Species", "Total_size", "QC", "ST", "arcC", "aroE", "glpF", "gmk", "pta", "tpi", "yqiL", "spa_type", #assembly module
"Sample", "Species", "Mash_distance","Total_size", "N_contig", "N50", "QC", #assembly module
"ST", "arcC", "aroE", "glpF", "gmk", "pta", "tpi", "yqiL", # st module
"spa_type", # spa module
"cap_type", "cap_completeness", "cap_genes", #capsule module
"sccmec_type", "sccmec_subtype", "sccmec_genes", #sccmec module
"agr_type", "agr_confidence", "agr_frameshifts", "agr_operon_status", #agr module
Expand Down
84 changes: 49 additions & 35 deletions staphscan/modules/assembly/assembly.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,35 @@ def __init__(self):

def check_db(self):
if not self.db_sketch.exists():
print("Warning: Mash sketch 'staph_refs.msh' not found. Run setup_mash.py.")
print(f"Warning: Mash sketch '{self.db_sketch.name}' not found. Skipping assembly module.")
return False
return True

def run(self, assembly_path):
contig_count, n50, longest, total_size, ambig = self.get_contig_stats(assembly_path)
species_call, mash_dist = self.run_mash(assembly_path)
# if db is missing skip and report na results
if not self.check_db():
return {
"Species": "Not Analyzed",
"Mash_distance": "-",
"Total_size": "-",
"QC": "SKIPPED (Missing DB)",
"N_contig": "-",
"N50": "-"
}
contig_count, n50, total_size, ambig = self.get_contig_stats(assembly_path)

species_call, mash_dist, is_mixed = self.run_mash(assembly_path)

qc_failures = []
if total_size < self.min_size: qc_failures.append("Too short")
if total_size > self.max_size: qc_failures.append("Too long")
if n50 < self.min_n50: qc_failures.append("Low_N50")
if "yes" in ambig: qc_failures.append("Ambiguous_Bases")

# Add contamination QC checks
if is_mixed: qc_failures.append("Mixed")
#if is_weak: qc_failures.append("Weak hit")

qc_status = "PASS"
if qc_failures:
qc_status = f"FAILED ({','.join(qc_failures)})"
Expand All @@ -37,55 +53,54 @@ def run(self, assembly_path):
"Mash_distance": mash_dist,
"Total_size": total_size,
"QC": qc_status,
"contig_count": contig_count,
"N50": n50,
"largest_contig": longest,
"ambiguous_bases": ambig
"N_contig": contig_count,
"N50": n50
}

def run_mash(self, assembly_path):
try:
cmd = ["mash", "dist", str(self.db_sketch), str(assembly_path)]
res = subprocess.run(cmd, capture_output=True, text=True)
if not res.stdout: return "Unknown", "-"
if not res.stdout: return "Unknown", "-", False

rows = []
for line in res.stdout.strip().split('\n'):
parts = line.split('\t')
ref_name = Path(parts[0]).name
dist = float(parts[2])
rows.append((ref_name, dist))

rows.sort(key=lambda x: x[1])
best_match = rows[0]
best_name = best_match[0]
best_dist = best_match[1]
display_name = "Unknown"
if "S_aureus" in best_name: display_name = "S. aureus"
elif "S_epidermidis" in best_name: display_name = "S. epidermidis"
elif "S_lugdunensis" in best_name: display_name = "S. lugdunensis"
elif "S_haemolyticus" in best_name: display_name = "S. haemolyticus"
elif "S_argenteus" in best_name: display_name = "S. argenteus"
elif "S_capitis" in best_name: display_name = "S. capitis"
elif "S_schweitzeri" in best_name: display_name = "S. schweitzeri"
else: display_name = best_name
# if best_dist <= 0.02:
# if display_name == "S. aureus":
# return "S. aureus (Strong match)", str(best_dist)
# else:
# return display_name, str(best_dist)
# elif best_dist <= 0.04:
# if display_name == "S. aureus":
# return "S. aureus (Weak match)", str(best_dist)
# else:
# return display_name, str(best_dist)
# else:
# return "No match found", str(best_dist)

# Contamination Logic: Check if multiple species are under 0.04 distance
close_matches = [r for r in rows if r[1] <= 0.04]
is_mixed = len(close_matches) > 1

# weak hits between 0.02 and 0.04
#is_weak = 0.02 < best_dist <= 0.04

species_map = {
"S_aureus": "S. aureus",
"S_epidermidis": "S. epidermidis",
"S_lugdunensis": "S. lugdunensis",
"S_haemolyticus": "S. haemolyticus",
"S_argenteus": "S. argenteus",
"S_capitis": "S. capitis",
"S_schweitzeri": "S. schweitzeri"
}

display_name = next((v for k, v in species_map.items() if k in best_name), best_name)

if best_dist <= 0.04:
return display_name, str(best_dist)
return display_name, str(best_dist), is_mixed
else:
return "No match found", str(best_dist)
return "No match found", str(best_dist), is_mixed

except Exception as e:
return f"Error ({e})", "-"
return f"Error ({e})", "-", False

def get_contig_stats(self, fasta_path):
lengths = []
Expand All @@ -107,10 +122,9 @@ def get_contig_stats(self, fasta_path):
lengths.append(len(s))
ambiguous_count += sum(1 for b in s if b.upper() not in "ATCG")

if not lengths: return 0, 0, 0, 0, "no"
if not lengths: return 0, 0, 0, "no"

lengths.sort()
longest = lengths[-1]
total = sum(lengths)
half = total / 2
cum_sum = 0
Expand All @@ -122,4 +136,4 @@ def get_contig_stats(self, fasta_path):
break

ambig_str = f"yes ({ambiguous_count})" if ambiguous_count > 0 else "no"
return len(lengths), n50, longest, total, ambig_str
return len(lengths), n50, total, ambig_str
Loading
Loading