Source code for nse_xbrl_parser.taxonomy.linkbase

"""
nse_xbrl_parser.taxonomy.linkbase

XBRL Linkbase Parsers:
- Label Linkbase (*-lab.xml): human readable labels for concepts
- Presentation Linkbase (*-pre.xml): UI tree and hierarchy
- Calculation Linkbase (*-cal.xml): summation-item relationships
"""
from __future__ import annotations

from typing import Any, Dict, List, Optional
from lxml import etree
from pydantic import BaseModel, Field

from ..utils.xml_utils import clean_xml_bytes, parse_xml_tree, strip_namespace

LINKBASE_NS = "http://www.xbrl.org/2003/linkbase"
XLINK_NS = "http://www.w3.org/1999/xlink"


class ConceptLabel(BaseModel):
    """Represents a human-readable concept label from a label linkbase."""

    concept_id: str
    label_role: str = "http://www.xbrl.org/2003/role/label"
    lang: str = "en"
    text: str


class CalculationArc(BaseModel):
    """Represents a parent-child calculation relationship."""

    parent: str
    child: str
    weight: float = 1.0
    order: Optional[float] = None


[docs] class TaxonomyLinkbase(BaseModel): """Container for parsed linkbase data.""" labels: Dict[str, str] = Field(default_factory=dict) # concept_name -> English label text calculations: List[CalculationArc] = Field(default_factory=list)
[docs] @classmethod def from_label_xml(cls, content: bytes | str) -> TaxonomyLinkbase: """Parse a label linkbase file (``*-lab.xml``, ``*-label.xml``).""" root = parse_xml_tree(content) labels: Dict[str, str] = {} # 1. Map locators (loc: label -> concept) loc_map: Dict[str, str] = {} for loc in root.xpath( "//*[local-name()='loc']", namespaces={"link": LINKBASE_NS, "xlink": XLINK_NS}, ): loc_label = loc.attrib.get(f"{{{XLINK_NS}}}label") or loc.attrib.get("label") loc_href = loc.attrib.get(f"{{{XLINK_NS}}}href") or loc.attrib.get("href") if loc_label and loc_href: # href typically looks like 'in-capmkt.xsd#in-capmkt_ScripCode' concept_name = loc_href.split("#")[-1] # strip standard prefixes concept_clean = concept_name.split("_")[-1] loc_map[loc_label] = concept_clean # 2. Map label arcs (labelArc: from loc to label resource) arc_map: Dict[str, str] = {} for arc in root.xpath( "//*[local-name()='labelArc']", namespaces={"link": LINKBASE_NS, "xlink": XLINK_NS}, ): arc_from = arc.attrib.get(f"{{{XLINK_NS}}}from") or arc.attrib.get("from") arc_to = arc.attrib.get(f"{{{XLINK_NS}}}to") or arc.attrib.get("to") if arc_from and arc_to: arc_map[arc_to] = arc_from # 3. Read label text for lbl in root.xpath( "//*[local-name()='label']", namespaces={"link": LINKBASE_NS, "xlink": XLINK_NS}, ): lbl_key = lbl.attrib.get(f"{{{XLINK_NS}}}label") or lbl.attrib.get("label") text = (lbl.text or "").strip() if not text or not lbl_key: continue # Determine concept name loc_label = arc_map.get(lbl_key, lbl_key) concept_name = loc_map.get(loc_label) if not concept_name: # Direct label heuristic (e.g. label_ScripCode -> ScripCode) if lbl_key.startswith("label_"): concept_name = lbl_key[len("label_") :] else: concept_name = lbl_key labels[concept_name] = text return cls(labels=labels)
[docs] def get_label(self, concept_name: str, default: Optional[str] = None) -> str: """Get the human-readable label for a concept name.""" clean = strip_namespace(concept_name) return self.labels.get(clean, default or clean)