Source code for pyvo.mivot.writer.instance
# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""
MivotInstance is a simple API for building MIVOT instances step by step.
A MIVOT instance is a MIVOT serialisation of an object whose attributes are set
with column values or literals.
A MIVOT instance can contain ATTRIBUTEs elements, COLLECTIONs of elements, or other INSTANCEs.
The MIVOT INSTANCE structure is defined by the data model on which the data is mapped.
"""
from pyvo.utils.prototype import prototype_feature
from pyvo.mivot.utils.exceptions import MappingError
from pyvo.mivot.utils.mivot_utils import MivotUtils
__all__ = ["MivotInstance"]
[docs]
@prototype_feature("MIVOT")
class MivotInstance:
    """
    API for building <INSTANCE> elements of MIVOT annotation step by step.
    This class provides methods for incremental construction of a MIVOT instance.
    It builds <INSTANCE> elements that can contain <ATTRIBUTE>, <INSTANCE>, and <REFERENCE>.
    Support for <COLLECTION> elements is not yet implemented.
    The main features are:
    - Model-agnostic: The implementation is independent of any specific data model.
    - Syntax validation: Ensures basic MIVOT syntax rules are followed.
    - Context-agnostic: Ignores context-dependent syntax rules.
    attributes
    ----------
    _dmtype : string
              Instance type (class VO-DML ID)
    _dmrole : string
              Role played by the instance in the context where it is used
              (given by the VO-DML serialization of the model)
    _dmid : string
            Free identifier of the instance
    """
    def __init__(self, dmtype, *, dmrole=None, dmid=None):
        """
        Parameters
        ----------
        dmtype : str
            dmtype of the INSTANCE (mandatory)
        dmrole : str, optional
            dmrole of the INSTANCE
        dmid : str, optional
            dmid of the INSTANCE
        Raises
        ------
        MappingError
            If ``dmtype`` is not provided
        """
        if not dmtype:
            raise MappingError("Cannot build an instance without dmtype")
        self._dmtype = dmtype
        self._dmrole = dmrole
        self._dmid = MivotUtils.format_dmid(dmid)
        self._content = []
    @property
    def dmid(self):
        return self._dmid
[docs]
    def add_attribute(self, dmtype=None, dmrole=None, *, value=None, unit=None):
        """
        Add an <ATTRIBUTE> element to the instance.
        Parameters
        ----------
        dmtype : str
            dmtype of the ATTRIBUTE (mandatory)
        dmrole : str
            dmrole of the ATTRIBUTE (mandatory)
        value : str or numerical, optional
            ID of the column to set the attribute value.
            If ref is a string starting with a * or is numerical,
            it is considered as a value (* stripped)
            as a ref otherwise
        unit : str, optional
            Unit of the attribute
        Raises
        ------
        MappingError
            If ``dmtype`` or ``dmrole`` is not provided, or if both ``ref`` and ``value`` are not defined
        """
        if not dmtype:
            raise MappingError("Cannot add an attribute without dmtype")
        if not dmrole:
            raise MappingError("Cannot add an attribute without dmrole")
        ref, literal = MivotUtils.get_ref_or_literal(value)
        if not ref and not value:
            raise MappingError("Cannot add an attribute without ref or value")
        xml_string = f'<ATTRIBUTE dmtype="{dmtype}" dmrole="{dmrole}" '
        if unit and unit != "None":
            xml_string += f'unit="{unit}" '
        if literal:
            xml_string += f'value="{literal}" '
        else:
            xml_string += f'ref="{ref}" '
        xml_string += " />"
        self._content.append(xml_string) 
[docs]
    def add_reference(self, dmrole=None, dmref=None):
        """
        Add a <REFERENCE> element to the instance.
        Parameters
        ----------
        dmrole : str
            dmrole of the REFERENCE (mandatory)
        dmref : str
            dmref of the REFERENCE (mandatory)
        Raises
        ------
        MappingError
            If ``dmrole`` or ``dmref`` is not provided
        """
        if not dmref:
            raise MappingError("Cannot add a reference without dmref")
        if not dmrole:
            raise MappingError("Cannot add a reference without dmrole")
        xml_string = f'<REFERENCE dmrole="{dmrole}" dmref="{dmref}" />'
        self._content.append(xml_string) 
[docs]
    def add_instance(self, mivot_instance):
        """
        Add a nested <INSTANCE> element to the instance.
        Parameters
        ----------
        mivot_instance : MivotInstance
            INSTANCE to be added
        Raises
        ------
        MappingError
            If ``mivot_instance`` is not of type ``MivotInstance``
        """
        if not isinstance(mivot_instance, MivotInstance):
            raise MappingError("Instance added must be of type MivotInstance")
        self._content.append(mivot_instance.xml_string()) 
[docs]
    def add_collection(self, dmrole, mivot_instances):
        """
        to be documented
        """
        dm_att = ""
        if dmrole:
            dm_att = f"dmrole=\"{dmrole}\""
        self._content.append(f'<COLLECTION {dm_att}>')
        for mivot_instance in mivot_instances:
            if isinstance(mivot_instance, MivotInstance):
                self._content.append(mivot_instance.xml_string())
            else:
                self._content.append(mivot_instance)
            self._content.append("\n")
        self._content.append("</COLLECTION>") 
[docs]
    def xml_string(self):
        """
        Build and serialize the <INSTANCE> element as a string.
        Returns
        -------
        str
            The string representation of the <INSTANCE> element
        """
        xml_string = f'<INSTANCE dmtype="{self._dmtype}" '
        if self._dmrole:
            xml_string += f'dmrole="{self._dmrole}" '
        if self._dmid:
            xml_string += f'dmid="{self._dmid}" '
        xml_string += ">\n"
        for element in self._content:
            xml_string += "   " + element + "\n"
        xml_string += "</INSTANCE>\n"
        return xml_string