Skip to content
Open
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
21 changes: 18 additions & 3 deletions seqnado/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,27 @@
Molecule,
Organism,
FileType,
GenomicCoordinate
)

from . import data, config, inputs, outputs
_LAZY_SUBMODULES = {"data", "config", "inputs", "outputs"}
_LAZY_ATTRS = {"GenomicCoordinate": ("config", "GenomicCoordinate")}


def __getattr__(name: str):
if name in _LAZY_SUBMODULES:
import importlib
mod = importlib.import_module(f".{name}", __name__)
globals()[name] = mod
return mod
if name in _LAZY_ATTRS:
mod_name, attr = _LAZY_ATTRS[name]
import importlib
mod = importlib.import_module(f".{mod_name}", __name__)
obj = getattr(mod, attr)
globals()[name] = obj
return obj
raise AttributeError(f"module 'seqnado' has no attribute {name!r}")


__all__ = [
"Assay",
Expand All @@ -49,5 +64,5 @@
"Molecule",
"Organism",
"FileType",
"GenomicCoordinate"
"GenomicCoordinate",
]
2 changes: 1 addition & 1 deletion seqnado/cli/autocomplete.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

def _assay_names() -> List[str]:
"""Get list of available assay names for autocomplete."""
from seqnado.inputs import Assay # local import to keep CLI startup snappy
from seqnado.core import Assay

return list(Assay.all_assay_clean_names())

Expand Down
20 changes: 10 additions & 10 deletions seqnado/cli/commands/benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,6 @@
from loguru import logger

from seqnado.cli.app_instance import app
from seqnado.cli.benchmark_helpers import (
compute_assay_output_sizes,
discover_snakemake_logs,
format_compact_number,
load_benchmark_table,
parse_alignment_processing_logs,
parse_snakemake_logs_timeline,
summarize_benchmarks,
write_html_report,
)
from seqnado.cli.utils import _configure_logging, cli_print_table, verbose_option


Expand Down Expand Up @@ -101,6 +91,16 @@ def run_benchmark_report(
verbose: bool = False,
) -> None:
"""Generate benchmark outputs from plain Python code without Typer option wrappers."""
from seqnado.cli.benchmark_helpers import (
compute_assay_output_sizes,
discover_snakemake_logs,
format_compact_number,
load_benchmark_table,
parse_alignment_processing_logs,
parse_snakemake_logs_timeline,
summarize_benchmarks,
write_html_report,
)
_configure_logging(verbose)

resolved_benchmark_dir = _resolve_benchmark_dir(benchmark_dir)
Expand Down
4 changes: 2 additions & 2 deletions seqnado/cli/commands/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@

from seqnado.cli.app_instance import app
from seqnado.cli.autocomplete import assay_autocomplete
from seqnado.cli.commands.benchmark import run_benchmark_report
from seqnado.cli.snakemake_builder import SnakemakeCommandBuilder
from seqnado.cli.utils import (
_configure_logging,
Expand All @@ -26,7 +25,6 @@
verbose_option,
preset_option,
)
from seqnado.outputs.multiomics import find_assay_config_paths
from seqnado.utils import (
resolve_profile_path,
extract_cores_from_options,
Expand All @@ -44,6 +42,7 @@ def _ensure_default_snakemake_flag(options: List[str], flag: str) -> List[str]:
def _run_benchmark_after_success(verbose: bool) -> None:
"""Best-effort benchmark/report generation after a successful pipeline run."""
try:
from seqnado.cli.commands.benchmark import run_benchmark_report
run_benchmark_report(benchmark_dir=Path(".benchmark"), verbose=verbose)
except typer.Exit as exc:
if exc.exit_code not in (0, None):
Expand Down Expand Up @@ -246,6 +245,7 @@ def pipeline(
require_snakemake()

# Detect multiomics configs early
from seqnado.outputs.multiomics import find_assay_config_paths
config_files = find_assay_config_paths(Path("."))
use_multiomics = len(config_files) > 1 and not config_file and not assay

Expand Down
2 changes: 2 additions & 0 deletions seqnado/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
SNPCallingConfig,
MCCConfig,
MethylationConfig,
GenomicCoordinate,
)

from .user_input import (
Expand Down Expand Up @@ -75,4 +76,5 @@
"MultiomicsConfig",
"build_multiomics_config",
"render_multiomics_configs",
"GenomicCoordinate",
]
27 changes: 26 additions & 1 deletion seqnado/config/configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@

from seqnado import (
Assay,
GenomicCoordinate,
MethylationMethod,
MotifMethod,
PCRDuplicateHandling,
Expand All @@ -32,6 +31,32 @@
)


class GenomicCoordinate(BaseModel):
"""Configuration for genomic coordinates."""

chromosome: str
start: int
end: int

@field_validator("start", "end")
def validate_coordinates(cls, v: int) -> int:
if v < 0:
raise ValueError("Genomic coordinates must be non-negative.")
return v

@field_validator("end")
def validate_end_greater_than_start(cls, v: int, info) -> int:
if "start" in info.data and v < info.data["start"]:
raise ValueError("End coordinate must be greater than start coordinate.")
return v

@classmethod
def from_string(cls, coord_str: str) -> "GenomicCoordinate":
chromosome, positions = coord_str.split(":")
start, end = map(int, positions.split("-"))
return cls(chromosome=chromosome, start=start, end=end)


def none_str_to_none(v):
if isinstance(v, str) and v.strip().lower() == "none":
return None
Expand Down
31 changes: 0 additions & 31 deletions seqnado/core.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
from enum import Enum

from pydantic import BaseModel, field_validator

# =============================================================================
# CONSTANTS
# =============================================================================
Expand Down Expand Up @@ -210,32 +208,3 @@ class LibraryType(Enum):
PAIRED = "paired-end"


class GenomicCoordinate(BaseModel):
"""Configuration for genomic coordinates."""

chromosome: str
start: int
end: int

@field_validator("start", "end")
def validate_coordinates(cls, v: int) -> int:
if v < 0:
raise ValueError("Genomic coordinates must be non-negative.")
return v

# Check that end is greater than start
@field_validator("end")
def validate_end_greater_than_start(cls, v: int, info) -> int:
# Only validate if start is available (i.e., it passed validation)
if "start" in info.data and v < info.data["start"]:
raise ValueError("End coordinate must be greater than start coordinate.")
return v

@classmethod
def from_string(cls, coord_str: str) -> "GenomicCoordinate":
"""
Create a GenomicCoordinate instance from a string representation.
"""
chromosome, positions = coord_str.split(":")
start, end = map(int, positions.split("-"))
return cls(chromosome=chromosome, start=start, end=end)
2 changes: 1 addition & 1 deletion tests/unit/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
import pytest
from pydantic import ValidationError

from seqnado.config import GenomicCoordinate
from seqnado.core import (
Assay,
GenomicCoordinate,
FileType,
PileupMethod,
DataScalingTechnique,
Expand Down
Loading