Source code for ptr.ptx

"""Planning timeline parser."""

from html.parser import HTMLParser
from pathlib import Path

from .block import ElementBlock, ObsBlock
from .datetime import EndTime, StartTime
from .element import Element
from .metadata import Comment, File, Metadata
from .prm import PointingRequestMessage
from .timeline import Timeline


ELEMENTS = {
    'startTime': StartTime,
    'endTime': EndTime,
    'comment': Comment,
    'metadata': Metadata,
    'timeline': Timeline,
}


def el_parser(element):
    """Read element and convert it if necessary.

    Warning
    -------
    Only the top level element is parsed, not its children.

    """
    if isinstance(element, Element):
        if element.tag in ELEMENTS:
            return ELEMENTS[element.tag](
                *element,
                description=element.description,
                **element.attrs,
            )

        if element.tag == 'include':
            return File(element.attrs['href'], description=element.description)

        if element.tag == 'block':
            return block_parser(element)

        if element.tag == 'prm':
            return prm_parser(element)

    return element


def _extract(element, key):
    """Extract a key from a element if when is not present."""
    try:
        return list(element.pop(key))
    except KeyError:
        return None


def block_parser(element):
    """Parse block element.

    Parameters
    ----------
    element: ptr.Element
        PTR block element.

    Returns
    -------
    ElementBlock, ObsBlock or None
        Parsed block element.

    Raises
    ------
    AttributeError
        If the block does not have a defined `href` attribute.

    Note
    ----
    SLEW blocks are discarded (appended automatically in Timeline).

    """
    ref = element.attrs.get('ref', '').upper()

    if not ref:
        raise AttributeError(f'Block without `ref` attribute:\n{element}')

    # Remove `ref` from element attributes
    del element.attrs['ref']

    # Skip slew block
    if ref == 'SLEW':
        return None

    # Check start time
    if 'startTime' not in element:
        raise KeyError(f'Block without `startTime` element:\n{element}')

    # Remove only the 1st direct child
    start = element.pop(element.index('startTime'))

    # Check end time
    if 'endTime' not in element:
        raise KeyError(f'Block without `endTime` element:\n{element}')

    # Remove only the 1st direct child
    end = element.pop(element.index('endTime'))

    # Extract if metadata if present
    metadata = _extract(element, 'metadata')

    if ref == 'OBS':
        return ObsBlock(
            start, end, *element,
            metadata=metadata,
            description=element.description,
            **element.attrs,
        )

    return ElementBlock(
        ref, start, end, *element,
        metadata=metadata,
        description=element.description,
        **element.attrs,
    )


def prm_parser(element):
    """Parse pointing request message element.

    Parameters
    ----------
    element: ptr.Element
        PTR pointing request message element.

    Returns
    -------
    PointingRequestMessage
        Parsed pointing request message element.

    """
    header = _extract(element, 'header')

    # Check is the segment is present
    if 'segment' not in element:
        raise KeyError(f'Pointing request message without `segment` element:\n{element}')

    segment = element.pop('segment')

    # Check is the timeline is present
    if 'timeline' not in segment:
        raise KeyError(f'Pointing request message without `timeline` segment:\n{segment}')

    timeline = segment.pop('timeline')

    seg_name = segment.attrs.get('name')
    seg_metadata = _extract(segment, 'metadata')

    return PointingRequestMessage(
        *timeline,
        header=header,
        description=element.description,
        seg_name=seg_name,
        seg_metadata=seg_metadata,
        **element.attrs,
    )


class PtxParser(HTMLParser):
    """PTX content parser."""

    def __init__(self):
        super().__init__()

        self.data = None
        self.elements = []
        self.desc = []

    @property
    def tag(self):
        """Get case-sensitive tag name."""
        tag = self.get_starttag_text()

        for c in '</>':
            tag = tag.replace(c, '')

        return tag.split()[0]

    def handle_starttag(self, _, attrs):
        """Handle tag opening."""
        el = Element(self.tag, **dict(attrs))

        if self.desc:
            el.description = self.desc
            self.desc = []

        self.elements.append(el)

    def handle_endtag(self, tag):
        """Handle tag closure."""
        last_tag = self.elements[-1].tag.lower()

        if tag != last_tag:
            raise KeyError(f'Closing </{tag}> tag before </{last_tag}>.')

        # Get the last element of the list and convert it if necessary
        el = el_parser(self.elements.pop())

        if el is not None:
            if self.elements:
                self.elements[-1].append(el)
            else:
                self.data = el

    def handle_startendtag(self, tag, attrs):
        """Handle empty tag."""
        self.handle_starttag(tag, attrs)
        self.handle_endtag(tag)

    def handle_data(self, data):
        """Handle data in tags."""
        if value := data.strip():
            self.elements[-1].append(value)

    def handle_comment(self, data):
        """Handle comment string."""
        if desc := data.strip():
            self.desc.append(desc)

    def error(self, message):
        """Parser error."""
        raise NotImplementedError


[docs]def read_ptx(ptx): """PTX file reader. Parameters ---------- ptx: str or pathlib.Path PTX file content text or file name. Returns ------- PointingRequestMessage Parsed pointing request content. """ content = str(ptx).strip() if not content.startswith('<'): content = Path(ptx).read_text(encoding='utf-8').strip() parser = PtxParser() parser.feed(content) return parser.data
# Alias `read_ptx` in `read_ptr` read_ptr = read_ptx