Source code for pysec2pri.parsers.hgnc

"""HGNC TSV file parser for secondary-to-primary identifier mappings.

This parser extracts:
1. ID-to-ID mappings: withdrawn/merged HGNC IDs -> current HGNC IDs
2. Label-to-label mappings: previous/alias labels -> current labels

Uses SSSOM-compliant MappingSet classes with cardinality computation.
"""

from __future__ import annotations

from pathlib import Path
from typing import ClassVar

import polars as pl
from sssom_schema import Mapping

from pysec2pri.parsers._nomenclature import (
    ALIAS_SYMBOL,
    DATE_SYMBOL_CHANGED,
    PREV_SYMBOL,
    STATUS,
    SYMBOL,
    GeneNomenclatureParser,
)
from pysec2pri.parsers.base import BaseMappingSet, LabelMappingSet

# HGNC column names (case-insensitive matching used)
HGNC_ID = "hgnc_id"

# Merged info column has different naming variants across HGNC file versions
MERGED_INFO_PATTERNS = [
    "merged_into_report(i.e. hgnc_id/symbol/status)",
    "merged_into_report(i.e hgnc_id/symbol/status)",
    "merged_into_report(s) (i.e hgnc_id|symbol|status)",
]


[docs] class HGNCParser(GeneNomenclatureParser): """Parser for HGNC TSV files using Polars for memory efficiency. Extracts secondary-to-primary HGNC identifier mappings and symbol mappings from HGNC withdrawn and complete set files. Returns: - IdMappingSet for ID-to-ID mappings (withdrawn/merged IDs) - LabelMappingSet for symbol mappings (alias/previous symbols) """ datasource_name = "hgnc" id_column: ClassVar[str] = HGNC_ID withdrawn_label_column: ClassVar[str] = SYMBOL merged_patterns: ClassVar[list[str]] = MERGED_INFO_PATTERNS def __init__( self, version: str | None = None, show_progress: bool = True, ): """Initialize the HGNC parser. Args: version: Version/release identifier for the datasource. show_progress: Whether to show progress bars during parsing. """ super().__init__(version=version, show_progress=show_progress)
[docs] def parse( self, input_path: Path | str | None, complete_set_path: Path | str | None = None, ) -> BaseMappingSet: """Parse HGNC withdrawn TSV file into an IdMappingSet. Args: input_path: Path to the withdrawn HGNC TSV file. complete_set_path: Optional path to the HGNC complete set TSV. When supplied, ``all_primary_ids|labels`` on the returned mapping set is populated with every current HGNC ID, not just those that appear as ``object_id`` in a withdrawn to primary mapping. Returns: IdMappingSet with computed cardinalities based on IDs. """ if input_path is None: raise ValueError("input_path must not be None") input_path = Path(input_path) self._resolve_version(input_path) mappings = self._parse_withdrawn(input_path) # Populate the full primary ID set when the complete set is available primary_ids = ( self._extract_primary_ids(Path(complete_set_path)) if complete_set_path is not None else None ) return self.create_mapping_set(mappings, mapping_type="id", primary_ids=primary_ids)
[docs] def parse_primary_labels( self, complete_set_path: Path | str | None, ) -> BaseMappingSet: """Return a mapping set whose only content is the full primary Symbol list. Reads the HGNC complete set to extract every current HGNC Symbol and stores it in ``_primary_labels``. The ``mappings`` list is intentionally left empty, this mapping set exists only to drive ``to_pri_labels()``. Args: complete_set_path: Path to the HGNC complete set TSV file. Returns: :class:`~pysec2pri.parsers.base.LabelMappingSet` with no mappings and ``_primary_labels`` populated with all current HGNC labels. """ if complete_set_path is None: raise ValueError("complete_set_path must not be None") complete_set_path = Path(complete_set_path) self._resolve_version(complete_set_path) return self.create_mapping_set( [], mapping_type="label", primary_labels=self._extract_primary_labels(complete_set_path), )
[docs] def parse_primary_ids( self, complete_set_path: Path | str | None, ) -> BaseMappingSet: """Return a mapping set whose only content is the full primary ID list. Reads the HGNC complete set to extract every current HGNC ID and stores it in ``_primary_ids``. The ``mappings`` list is intentionally left empty, this mapping set exists only to drive ``to_pri_ids()``. Args: complete_set_path: Path to the HGNC complete set TSV file. Returns: :class:`~pysec2pri.parsers.base.IdMappingSet` with no mappings and ``_primary_ids`` populated with all current HGNC IDs. """ if complete_set_path is None: raise ValueError("complete_set_path must not be None") complete_set_path = Path(complete_set_path) self._resolve_version(complete_set_path) return self.create_mapping_set( [], mapping_type="id", primary_ids=self._extract_primary_ids(complete_set_path), )
[docs] def parse_labels( self, complete_set_path: Path | str | None, statuses: list[str] | None = None, ) -> LabelMappingSet: """Parse HGNC complete set for symbol (label) mappings. Args: complete_set_path: Path to the complete HGNC set TSV file. statuses: Entry statuses to include (e.g. ``["Approved"]``). If ``None`` (default), all entries are included. Returns: LabelMappingSet with computed cardinalities based on labels. """ if complete_set_path is None: raise ValueError("complete_set_path must not be None") complete_set_path = Path(complete_set_path) self._resolve_version(complete_set_path) mappings = self._parse_complete_set(complete_set_path, statuses=statuses) return self.create_mapping_set( mappings, mapping_type="label", primary_labels=self._extract_primary_labels(complete_set_path), )
[docs] def parse_all( self, withdrawn_path: Path | str | None, complete_set_path: Path | str | None, ) -> tuple[BaseMappingSet, BaseMappingSet]: """Parse both withdrawn and complete set files. Args: withdrawn_path: Path to the withdrawn HGNC TSV file. complete_set_path: Path to the complete HGNC set TSV file. Returns: Tuple of (IdMappingSet, LabelMappingSet). """ id_mappings = self.parse(withdrawn_path) label_mappings = self.parse_labels(complete_set_path) return id_mappings, label_mappings
def _extract_primary_labels(self, file_path: Path) -> dict[str, set[str]]: """Extract all current HGNC Symbols from the complete set file. Returns a ``dict`` mapping each symbol text to the set of primary HGNC IDs that carry that symbol. Args: file_path: Path to the HGNC complete set TSV file. Returns: ``dict[label, set[hgnc_id]]`` """ df = self._read_tsv(file_path) sym_col = self._find_column(df.columns, SYMBOL) id_col = self._find_column(df.columns, self.id_column) status_col = self._find_column(df.columns, STATUS) if sym_col is None: raise ValueError(f"Could not find hgnc_symbol column in {file_path}") if id_col is None: raise ValueError(f"Could not find hgnc_id column in {file_path}") approved = df.filter(pl.col(status_col) == "Approved") if status_col else df return self._labels_by_id(approved, id_col, sym_col) def _parse_complete_set( self, file_path: Path, statuses: list[str] | None = None ) -> list[Mapping]: """Parse complete HGNC set for symbol (label) mappings. Args: file_path: Path to the complete HGNC set TSV file. statuses: Entry statuses to include (e.g. ``["Approved"]``). If ``None`` (default), all entries are included. Returns: List of SSSOM Mapping objects for label mappings. """ df = self._read_tsv(file_path) status_col = self._find_column(df.columns, STATUS) id_col = self._find_column(df.columns, self.id_column) label_col = self._find_column(df.columns, SYMBOL) alias_col = self._find_column(df.columns, ALIAS_SYMBOL) prev_col = self._find_column(df.columns, PREV_SYMBOL) date_changed_col = self._find_column(df.columns, DATE_SYMBOL_CHANGED) if not all([status_col, id_col, label_col]): raise ValueError(f"Missing required columns in {file_path}") assert id_col is not None assert label_col is not None if statuses is not None and status_col: df = df.filter(pl.col(status_col).is_in(statuses)) return self._build_label_mappings( df, id_col=id_col, label_col=label_col, alias_col=alias_col, prev_col=prev_col, date_changed_col=date_changed_col, )
__all__ = ["HGNCParser"]