Source code for elsheeto.parser.stage2

"""Implementation of stage 2 parser.

Stage 2 converts the raw sectioned data from stage 1 into structured content:
- Key-value sections (Header, Settings, etc.) become HeaderSection objects
- Tabular sections (Data, Samples, etc.) become DataSection objects

The parser determines section types based on content structure and known patterns.
"""

import logging

from elsheeto.models.csv_stage1 import ParsedRawSection, ParsedRawSheet
from elsheeto.models.csv_stage2 import DataSection, HeaderSection, ParsedSheet
from elsheeto.parser.common import ParserConfiguration

#: The module logger.
LOGGER = logging.getLogger(__name__)


[docs] class Parser: """Stage 2 parser that converts raw sectioned data into structured content. Converts ParsedRawSheet (stage 1) into ParsedSheet (stage 2) by: - Identifying section types (key-value vs tabular) - Converting key-value sections to HeaderSection objects - Converting tabular sections to DataSection objects - Applying configuration-based transformations """
[docs] def __init__(self, config: ParserConfiguration) -> None: """Initialize the parser with the given configuration. Args: config: Parser configuration to use. """ self.config = config
[docs] def parse(self, *, raw_sheet: ParsedRawSheet) -> ParsedSheet: """Convert raw sectioned data into structured content. Args: raw_sheet: The raw parsed sheet from stage 1. Returns: The structured parsed sheet. """ LOGGER.debug("Converting stage 1 raw sheet to stage 2 structured content") header_sections = [] data_section = None if not raw_sheet.sections: # No sections at all - create empty data section data_section = DataSection(headers=[], header_to_index={}, data=[]) LOGGER.debug("No sections found, created empty data section") elif len(raw_sheet.sections) == 1 and raw_sheet.sections[0].name == "": # Sectionless sheet - single unnamed section is the data section data_section = self._convert_to_data_section(raw_sheet.sections[0]) LOGGER.debug("Sectionless sheet: unnamed section treated as data section") else: # Sectioned sheet - last section is data, all others are headers # Process all sections except the last one as header sections for section in raw_sheet.sections[:-1]: header_section = self._convert_to_header_section(section) if header_section: # Only add non-empty header sections header_sections.append(header_section) # Last section is always the data section if raw_sheet.sections: data_section = self._convert_to_data_section(raw_sheet.sections[-1]) LOGGER.debug("Sectioned sheet: last section treated as data section") # Ensure we always have a data section if data_section is None: # pragma: no cover data_section = DataSection(headers=[], header_to_index={}, data=[]) LOGGER.debug("No data section created, using empty data section") return ParsedSheet( delimiter=raw_sheet.delimiter, sheet_type=raw_sheet.sheet_type, header_sections=header_sections, data_section=data_section, )
def _convert_to_header_section(self, section: ParsedRawSection) -> HeaderSection | None: """Convert a raw section to a header section (preserving original row structure). Args: section: The raw section to convert. Returns: HeaderSection object or None if section is empty. """ rows = [] for row in section.data: if not row or all(cell.strip() == "" for cell in row): continue # Skip empty rows # Simply preserve the row as-is (no more validation on number of fields) rows.append(row) return HeaderSection(name=section.name.lower(), rows=rows) if rows else None def _convert_to_data_section(self, section: ParsedRawSection) -> DataSection: """Convert a raw section to a data section (tabular data). Args: section: The raw section to convert. Returns: DataSection object. """ if not section.data: return DataSection(headers=[], header_to_index={}, data=[]) # First row is typically headers headers = [] data_rows = [] if section.data: # Extract headers from first row header_row = section.data[0] headers = [cell.strip() for cell in header_row] # Remaining rows are data data_rows = section.data[1:] # Create header to index mapping header_to_index = {header: idx for idx, header in enumerate(headers)} return DataSection( headers=headers, header_to_index=header_to_index, data=data_rows, )
[docs] def from_stage1(*, raw_sheet: ParsedRawSheet, config: ParserConfiguration) -> ParsedSheet: """Convert raw sectioned data into structured content. Args: raw_sheet: The raw parsed sheet from stage 1. config: Parser configuration to use. Returns: The structured parsed sheet. """ parser = Parser(config) return parser.parse(raw_sheet=raw_sheet)