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
37 changes: 33 additions & 4 deletions hAMRonization/Interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@
import csv
import json
import argparse
import logging
import traceback
import dataclasses
from abc import ABC, abstractmethod
import hAMRonization
import hAMRonization.summarize
from .hAMRonizedResult import hAMRonizedResult

logger = logging.getLogger(__name__)


class hAMRonizedResultIterator(ABC):
"""
Expand Down Expand Up @@ -39,16 +43,27 @@ def __init__(self, source, field_map, metadata):

try:
if os.stat(source).st_size == 0:
print(f"Warning: {source} is empty", file=sys.stderr)
logger.warning("Input file %s is empty", source)
self.stream = open(source, "r")
except FileNotFoundError: # path doesn't exist
print(f"File {source} not found", file=sys.stderr)
exit(1)
except FileNotFoundError:
logger.error("File not found: %s", source)
sys.exit(1)

try:
self.hAMRonized_results = self.parse(self.stream)
except KeyError as e:
self.stream.close()
logger.error(
"Expected column %s not found in %s. "
"Please check you are using the correct AMR "
"prediction tool output file.",
e, source
)
logger.debug("Full traceback:\n%s", traceback.format_exc())
sys.exit(1)
except Exception:
self.stream.close()
raise

# TODO: the field_map_override is a half-hack to support the scenario
# (as for amrfinderplus) where different records need different mappings,
Expand Down Expand Up @@ -242,6 +257,13 @@ def generic_cli_interface():
version=f"%(prog)s {hAMRonization.__version__}",
)

parser.add_argument(
"--debug",
action="store_true",
default=False,
help="Enable debug mode with full tracebacks",
)

# add tool specific parsers
subparser = parser.add_subparsers(
title="Tools with hAMRonizable reports", help="", dest="analysis_tool"
Expand Down Expand Up @@ -280,6 +302,13 @@ def generic_cli_interface():

args = parser.parse_args()

if args.debug:
logging.basicConfig(level=logging.DEBUG,
format="%(levelname)s:%(name)s:%(message)s")
else:
logging.basicConfig(level=logging.WARNING,
format="%(levelname)s: %(message)s")

if args.analysis_tool and args.analysis_tool != "summarize":
required_mandatory_metadata = hAMRonization._RequiredToolMetadata[
args.analysis_tool
Expand Down
47 changes: 46 additions & 1 deletion hAMRonization/hAMRonizedResult.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,35 @@
#!/usr/bin/env python

import os
import logging
import dataclasses

logger = logging.getLogger(__name__)

# Fields where upstream tools may report multiple semicolon-separated values
# or dash-separated ranges instead of a single number (e.g., RGI-bwt).
# For these fields we extract the first numeric value rather than failing.
_MULTI_VALUE_NUMERIC_FIELDS = {
"reference_gene_length",
"sequence_identity",
}


def _extract_first_numeric(value, target_type):
"""
Given a string that may contain semicolon-separated values
(e.g., '3561; 3564; 3570') or dash-separated ranges
(e.g., '92.82 - 100.0'), extract and return the first numeric
value cast to target_type.
"""
raw = str(value).strip()
for sep in [";", " - ", "-"]:
if sep in raw:
first = raw.split(sep)[0].strip()
if first:
return target_type(first)
return target_type(raw)


@dataclasses.dataclass
class hAMRonizedResult:
Expand Down Expand Up @@ -64,7 +91,25 @@ def __post_init__(self):
if not isinstance(value, field.type) and value:
try:
setattr(self, field.name, field.type(value))
except ValueError:
except (ValueError, TypeError):
if field.name in _MULTI_VALUE_NUMERIC_FIELDS:
try:
extracted = _extract_first_numeric(
value, field.type
)
setattr(self, field.name, extracted)
logger.debug(
"Field '%s' contained multiple values "
"(%r), extracted first: %s",
field.name, value, extracted
)
continue
except (ValueError, TypeError):
pass
logger.error(
"Expected %s to be %s, got %r",
field.name, field.type, value
)
raise ValueError(
f"Expected {field.name} "
f"to be {field.type}, "
Expand Down
2 changes: 2 additions & 0 deletions test/data/dummy/rgi/rgi_bwt.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ARO Term ARO Accession Reference Model Type Reference DB Alleles with Mapped Reads Reference Allele(s) Identity to CARD Reference Protein (%) Resistomes & Variants: Observed in Genome(s) Resistomes & Variants: Observed in Plasmid(s) Resistomes & Variants: Observed Pathogen(s) Completely Mapped Reads Mapped Reads with Flanking Sequence All Mapped Reads Average Percent Coverage Average Length Coverage (bp) Average MAPQ (Completely Mapped Reads) Number of Mapped Baits Number of Mapped Baits with Reads Average Number of reads per Bait Number of reads per Bait Coefficient of Variation (%) Number of reads mapping to baits and mapping to complete gene Number of reads mapping to baits and mapping to complete gene (%) Mate Pair Linkage (# reads) Reference Length AMR Gene Family Drug Class Resistance Mechanism
Bifidobacterium adolescentis rpoB mutants conferring resistance to rifampicin 3004480 protein homolog model CARD; Resistomes & Variants 15 92.82 - 100.0 YES no data Bifidobacterium adolescentis 668 0 668 34.84 1249.47 116.48 0 0 0 0 N/A N/A N/A 3561; 3561; 3564; 3570 rifamycin-resistant beta-subunit of RNA polymerase (rpoB) rifamycin antibiotic antibiotic target alteration; antibiotic target replacement
50 changes: 50 additions & 0 deletions test/test_parsing_validity.py
Original file line number Diff line number Diff line change
Expand Up @@ -992,3 +992,53 @@ def test_fargene():
assert result.input_protein_start == 7
assert result.input_protein_stop == 118
assert result.input_protein_length == 140


def test_rgi_bwt_multi_value_fields():
"""Test that RGI-bwt output with multi-value reference_gene_length
and range-style sequence_identity is parsed correctly (issue #84)."""
metadata = {
"analysis_software_version": "6.0.1",
"reference_database_version": "3.2.8",
"input_file_name": "rgi_bwt_test",
}
parsed_report = hAMRonization.parse(
"data/dummy/rgi/rgi_bwt.txt", metadata, "rgi"
)

for result in parsed_report:
assert result.input_file_name == "rgi_bwt_test"
assert result.gene_symbol == (
"Bifidobacterium adolescentis rpoB mutants conferring "
"resistance to rifampicin"
)
assert result.reference_accession == "3004480"
assert result.analysis_software_name == "rgi"
assert result.reference_database_name == "CARD; Resistomes & Variants"

# issue #84: these fields contain multi-values in RGI-bwt output
# reference_gene_length '3561; 3561; 3564; 3570' -> first value 3561
assert result.reference_gene_length == 3561
assert isinstance(result.reference_gene_length, int)

# sequence_identity '92.82 - 100.0' -> first value 92.82
assert result.sequence_identity == 92.82
assert isinstance(result.sequence_identity, float)

assert result.coverage_percentage == 34.84
assert result.input_gene_length == 1249
assert result.drug_class == "rifamycin antibiotic"
assert result.resistance_mechanism == (
"antibiotic target alteration; antibiotic target replacement"
)


def test_extract_first_numeric():
"""Unit test for the multi-value numeric field parser."""
from hAMRonization.hAMRonizedResult import _extract_first_numeric

assert _extract_first_numeric("3561; 3564; 3570", int) == 3561
assert _extract_first_numeric("92.82 - 100.0", float) == 92.82
assert _extract_first_numeric("42", int) == 42
assert _extract_first_numeric("99.5", float) == 99.5
assert _extract_first_numeric("100; 200", int) == 100
Loading