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
43 changes: 43 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,47 @@ jobs:
files: ./coverage.xml
fail_ci_if_error: false

min-deps:
# Installs every runtime dependency pinned to its declared lower bound
# (e.g. typer==0.12.4) and runs the CLI smoke tests. Guards against the
# floor being too low.
runs-on: ubuntu-latest

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

- name: Set up Python
uses: actions/setup-python@v7
with:
python-version: '3.11'
cache: pip

- name: Build minimum-dependency constraints file
shell: python
run: |
import re, tomllib
from pathlib import Path

pyproject = tomllib.loads(Path('pyproject.toml').read_text())
deps = pyproject['project']['dependencies']
# Each entry looks like "typer>=0.12.4" (some carry a trailing comment).
# Pin to the declared lower bound: "typer>=0.12.4" -> "typer==0.12.4".
pins = []
for dep in deps:
m = re.match(r"^\s*([A-Za-z0-9_.-]+)\s*>=\s*([0-9][0-9A-Za-z.\-]*)", dep)
if not m:
raise SystemExit(f"could not parse lower bound: {dep!r}")
pins.append(f"{m.group(1)}=={m.group(2)}")
Path('min-constraints.txt').write_text("\n".join(pins) + "\n")
print("\n".join(pins))

- name: Install minimum runtime dependencies
run: |
python -m pip install --upgrade pip
python -m pip install -c min-constraints.txt -e ".[dev]"

- name: Run CLI smoke tests against minimum dependencies
run: pytest tests/test_cli_smoke.py -v


3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,5 @@ graphify-out
coverage.xml
TODO.md
production/
monitoring/
monitoring/
docs/RELEASE_SUMMARY_RESPRO.md
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,15 @@ conda create -n respro
conda activate respro
conda install bioconda::respro
```

Install via Docker (BioContainers):

```bash
docker pull quay.io/biocontainers/respro
```

Install via pip:

```bash
git clone https://github.com/the-foxlab/ResistanceProfiler
pip install -e ".[dev]"
Expand Down
38 changes: 38 additions & 0 deletions docs/docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,21 @@ respro init \

If your dataset only contains atomic mutation rules, omit `--formula-rules`.

Optionally ship a per-database example consensus FASTA that users can profile
with a single command (see [Profile FASTA input](#profile-fasta-input)) and that
the webapp exposes as an "Example" button. The example must be a single-record
FASTA:

```bash
respro init \
--name "Docs Demo" \
--genbank some_reference.gb \
--rules rules.tsv \
--formula-rules combinatorial_rules.tsv \
--example example_consensus.fasta \
--output myrespro.db
```

For metadata and interpretation algorithm options, see [Database Preparation](database-preparation.md).

## Extend or validate rules in an existing project
Expand All @@ -79,6 +94,16 @@ respro add \
--formula-rules combinatorial_rules.tsv
```

Use `--example example_consensus.fasta` to store or overwrite a per-database
example FASTA, or `--no-example` to clear a previously stored example:

```bash
respro add \
--project myrespro.db \
--rules rules.tsv \
--example example_consensus.fasta
```

## Profile FASTA input

```bash
Expand All @@ -91,6 +116,19 @@ respro fasta \
--export pdf
```

To profile the example consensus FASTA stored in the project database (set via
`respro init --example` or `respro add --example`), use `--example` instead of
`--fasta`. The two options are mutually exclusive:

```bash
respro fasta \
--project myrespro.db \
--example \
--output my_output
```

If no example is stored, the command fails with a message.

## Profile VCF input

```bash
Expand Down
1 change: 1 addition & 0 deletions docs/docs/database-preparation.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Notes:
- `--genbank` can be repeated for multiple files.
- `--no-additional-info` skips network lookups for extra metadata.
- `--metadata` accepts a JSON file with curated project metadata. See the section below for the supported keys and value rules.
- `--example` optionally stores a single-record consensus FASTA shipped with the database. Users can then profile it via `respro fasta --example` or via the webapp "Example" button. Use `respro add --example` to overwrite and `respro add --no-example` to clear it.
- After initialization, later profiling runs use this database as the internal coordinate and rule source.

After this command succeeds, the file `myrespro.db` should exist.
Expand Down
13 changes: 13 additions & 0 deletions docs/docs/how-it-works.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,19 @@ For VCF input, variants are defined in user-provided reference coordinates. ResP
- For reverse-strand features, alleles are reverse-complemented to the internal forward strand.
- Anchor-changed indels (where the VCF anchor base differs from the internal reference) are automatically split into a substitution plus a canonical indel before annotation.

#### Spliced genes and unspliced queries

When a project feature is a spliced CDS (more than one segment in `feature_segment`) and the user supplies an unspliced whole-genome query, minimap2 reports the inter-exon intron as a single large `I` (FASTA) or `D` (raw mappy) operation in the CIGAR. Without special handling this intron would be misinterpreted as a giant coding insertion (e.g. a multi-kilobase frameshift) and would crash per-exon identity.

ResPro classifies such intron operations:

- The exon-junction CDS offsets are derived from `feature.segments` in genomic 5'→3' order (matching the normalized CIGAR's walking order for both strands).
- A CIGAR `I` op is classified as an intron when **both** hold: (1) its CDS position coincides with a known junction offset within `alignment.intron_junction_tolerance` (default 5 nt; configurable in `defaults.toml`), and (2) its length is strictly greater than that same tolerance.
- Classified intron `I` ops are removed from the CIGAR stored on the `FeatureMatch` (producing an exon-only CIGAR) and recorded as `IntronInterval`s (CDS junction position, query span, length).
- Identity and CDS coverage are recomputed over exons only, so a perfect exons match reports ~99% identity rather than the genomic span including the intron.
- In FASTA mode the intron query span is stripped from the region before codon walking, so no intron insertion variant is emitted. Real coding insertions (any `I` op not at a junction within tolerance, or of length ≤ tolerance) are still emitted.
- In VCF mode the query-to-CDS coordinate map skips intron query positions and offsets exon-2 (and later) CDS positions past the intron span, so variants inside the intron are excluded and exon-2 variants remap to the correct CDS offset.

### Amino-acid consequence interpretation

Per-feature nucleotide changes are translated into amino-acid consequences. Supported classes include:
Expand Down
8 changes: 8 additions & 0 deletions docs/docs/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ respro --version

Bioconda is the recommended install path if you already use conda/mamba — it handles the `mappy` native dependency automatically without a C compiler.

### via BioContainers (Docker)

```bash
docker pull quay.io/biocontainers/respro
```

The BioContainers image provides a containerized CLI environment. Mount your data directories and run `respro` commands inside the container.

## Web app

### Docker (recommended)
Expand Down
11 changes: 10 additions & 1 deletion docs/docs/webapp.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ ships pre-built `.db` files does **not** need access to any of them.

| Phase | Host | Purpose |
|---|---|---|
| Maintained-DB bootstrap & updates | `raw.githubusercontent.com` | Fetch `manifest.json`, `rules.tsv`, `metadata.json`, `formula-rules.tsv` from `the-foxlab/respro-databases` |
| Maintained-DB bootstrap & updates | `raw.githubusercontent.com` | Fetch `manifest.json`, `rules.tsv`, `metadata.json`, `formula-rules.tsv`, and (when declared) `example.fasta` from `the-foxlab/respro-databases` |
| Maintained-DB bootstrap | `eutils.ncbi.nlm.nih.gov` | Fetch GenBank reference records (`efetch.fcgi?db=nuccore`) referenced by a database's rules |
| `respro init` enrichment | `pubchem.ncbi.nlm.nih.gov` | Resolve drug names to CIDs, descriptions, titles, structure images (PUG REST) |
| `respro init` enrichment | `eutils.ncbi.nlm.nih.gov` | PubMed article summaries (`esummary.fcgi`), PMC ID conversion |
Expand Down Expand Up @@ -216,6 +216,15 @@ In batch VCF mode, an optional BAM can be attached to each sample for per-sample
| `RESPRO_WEB_MAINTAINED_BOOTSTRAP` | `false` | When `true` (or `1`/`yes`/`on`), missing maintained databases are downloaded into `data/project_databases/` at startup, and a weekly background thread checks for updates. |
| `RESPRO_WEB_MAINTAINED_DB_UPDATE_INTERVAL_SECONDS` | `604800` (7 days) | Interval between maintained-database update checks. Set to `0` to disable the weekly thread (a one-time check still runs at startup when bootstrap is enabled). Invalid values fall back to the default. |

A maintained database may declare an optional `example_fasta_path` field in its
`manifest.json` entry. When present, the referenced single-record consensus
FASTA is downloaded as `example.fasta` alongside the rules and stored in the
project database. The webapp then shows an **Example** button next to the
"One sample" / "Multiple samples" choice in the Analyze tab for that database;
clicking it switches to FASTA mode and profiles the stored example in one step,
without requiring any user-supplied input file. The button is only shown for
databases that actually ship an example.

### Minimal `.env` example

For local development you can usually leave everything at defaults. For a public deployment, start from:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ classifiers = [
]
dependencies = [
'click>=8.1',
'typer>=0.12',
'typer>=0.12.4',
'rich>=13.0',
'numpy>=1.24',
'biopython>=1.81',
Expand Down
41 changes: 39 additions & 2 deletions respro/cli/fasta.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from __future__ import annotations

import logging
import os
import tempfile
from pathlib import Path
from typing import Annotated

Expand All @@ -25,6 +27,7 @@
from respro.core.annotation import annotate_variants
from respro.core.fasta_to_vcf import fasta_to_vcf
from respro.core.query import resolve_fasta_query
from respro.db.rules_queries import get_project_example_fasta
from respro.db.schema import open_project_db
from respro.utils.cli_errors import cli_error, render_click_exception
from respro.utils.logging import err_console
Expand All @@ -36,9 +39,16 @@ def _profile_fasta_command(
typer.Option('--project', '-p', exists=True, help='Project database.')
],
consensus_fasta: Annotated[
Path,
Path | None,
typer.Option('--fasta', '-f', exists=True, help='Input consensus FASTA sequence.')
],
] = None,
use_example: Annotated[
bool,
typer.Option(
'--example',
help='Profile the example consensus FASTA stored in the project database.',
)
] = False,
sample: Annotated[
str,
typer.Option('--sample', '-s', help='Sample name for the report.')
Expand Down Expand Up @@ -87,6 +97,12 @@ def _profile_fasta_command(
project_conn = None
results_conn = None

if use_example and consensus_fasta is not None:
cli_error('Provide either --fasta or --example, not both.')
if not use_example and consensus_fasta is None:
cli_error('Provide --fasta or --example to specify the input consensus sequence.')

example_temp_path: Path | None = None
try:
export_formats = _parse_export_formats(export)

Expand All @@ -95,6 +111,25 @@ def _profile_fasta_command(
if project_row is None:
cli_error('No project found in the database')

# When --example is used, materialise the stored example FASTA text into a temp file so the
# existing read/align pipeline is reused unchanged.
if use_example:
example_text = get_project_example_fasta(project_conn)
if example_text is None:
cli_error(
f'No example FASTA is stored in project database {project!s}. '
'Add one with `respro init --example <fasta>`.'
)
assert example_text is not None
temp_fd, temp_name = tempfile.mkstemp(prefix='respro_example_', suffix='.fasta')
example_temp_path = Path(temp_name)
os.close(temp_fd)
example_temp_path.write_text(example_text)
consensus_fasta = example_temp_path

# cli_error raises typer.Exit, but mypy cannot infer that; narrow explicitly.
assert consensus_fasta is not None

results_conn = _init_results_db_connection(
str(results_db) if results_db else None, project_conn, logger,
)
Expand Down Expand Up @@ -163,6 +198,8 @@ def _profile_fasta_command(
project_conn.close()
if results_conn is not None:
results_conn.close()
if example_temp_path is not None:
example_temp_path.unlink(missing_ok=True)


def register(app: typer.Typer) -> None:
Expand Down
Loading
Loading