diff --git a/astropy/io/votable/exceptions.py b/astropy/io/votable/exceptions.py index 610c80e6518..58a68d2be10 100644 --- a/astropy/io/votable/exceptions.py +++ b/astropy/io/votable/exceptions.py @@ -255,6 +255,12 @@ class VOTableSpecWarning(VOWarning, SyntaxWarning): """ +class ModelMappingSpecWarning(VOWarning, SyntaxWarning): + """ + The input model mapping XML mapping block violates the spec. + """ + + class UnimplementedWarning(VOWarning, SyntaxWarning): """ A feature of the VOTABLE_ spec is not implemented. @@ -1524,6 +1530,14 @@ class E25(VOTableSpecWarning): message_template = "No FIELDs are defined; DATA section will be ignored." +class E26(VOTableSpecError): + """ + The mapping block can only be set in a type=meta RESOURCE. + """ + + message_template = "Mapping block can not be set in a RESOURCE with type=result" + + def _get_warning_and_exception_classes(prefix): classes = [] for key, val in globals().items(): diff --git a/astropy/io/votable/tests/data/mivot_annotated_table.xml b/astropy/io/votable/tests/data/mivot_annotated_table.xml new file mode 100644 index 00000000000..c8a6858bb1f --- /dev/null +++ b/astropy/io/votable/tests/data/mivot_annotated_table.xml @@ -0,0 +1,352 @@ + + + + This schema contains data re-published from the official... + + + + + + + This schema contains data re-published from the official Gaia mirrors + ... + + If you use public Gaia DR3 data in a paper, please take note of +`ESAC's guide`_ on how to acknowledge and cite it. + +.. _ESAC's guide: +https://gea.esac.esa.int/archive/documentation/GDR3/Miscellaneous/sec_credit_and_citation_instructions/ + This is gaia_source from the Gaia Data Release 3, stripped to just + ... + + Query successful + For advice on how to cite the resource(s) that contributed to this result, see http://dc.zah.uni-heidelberg.de/tableinfo/gaia.dr3lite#ti-citing + More information on a resource that contributed to this data is found at http://dc.zah.uni-heidelberg.de/tableinfo/gaia.dr3lite + + + + + + + hand-made mapping + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This is gaia_source from the Gaia Data Release 3, stripped to just... + + + + For the contents of Gaia DR3,... + + + + + Gaia DR3 unique source identifier. Note that this *cannot* be matched against the DR1 or DR2 source_ids. + + + + + + + + Mean magnitude in the G band. This is computed from the G-band mean flux applying the magnitude + zero-point + in the Vega scale. To obtain error estimates, see phot_g_mean_flux_over_error. + + + + + + + + Mean magnitude in the integrated RP band. This is computed from the RP-band mean flux applying the + magnitude zero-point in the Vega scale. To obtain error estimates, see phot_rp_mean_flux_over_error. + + + + + + + + Mean magnitude in the integrated BP band. This is computed from the BP-band mean flux applying the + magnitude zero-point in the Vega scale. To obtain error estimates, see phot_bp_mean_flux_over_error. + + + + + + + + + + + + + + + + + + + + + + +
216509215492473292819.63309718.23066321.65443
216509215922651468820.99798220.10654621.16427
+
+
\ No newline at end of file diff --git a/astropy/io/votable/tests/data/mivot_block_custom_datatype.xml b/astropy/io/votable/tests/data/mivot_block_custom_datatype.xml new file mode 100644 index 00000000000..ee8707806ef --- /dev/null +++ b/astropy/io/votable/tests/data/mivot_block_custom_datatype.xml @@ -0,0 +1,287 @@ + + + hand-made mapping + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/astropy/io/votable/tests/data/test.order.xml b/astropy/io/votable/tests/data/test.order.xml new file mode 100644 index 00000000000..6f705da3dfa --- /dev/null +++ b/astropy/io/votable/tests/data/test.order.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + +
+ + + + + + +
+ + + diff --git a/astropy/io/votable/tests/test_tree.py b/astropy/io/votable/tests/test_tree.py index 0d02aad00fd..4421a22bed6 100644 --- a/astropy/io/votable/tests/test_tree.py +++ b/astropy/io/votable/tests/test_tree.py @@ -1,13 +1,14 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst +import filecmp import io from contextlib import nullcontext import pytest from astropy.io.votable import tree -from astropy.io.votable.exceptions import W07, W08, W21, W41 +from astropy.io.votable.exceptions import E26, W07, W08, W21, W41 from astropy.io.votable.table import parse -from astropy.io.votable.tree import Resource, VOTableFile +from astropy.io.votable.tree import MivotBlock, Resource, VOTableFile from astropy.tests.helper import PYTEST_LT_8_0 from astropy.utils.data import get_pkg_data_filename from astropy.utils.exceptions import AstropyDeprecationWarning @@ -175,3 +176,315 @@ def test_votable_tag(): assert 'xmlns="http://www.ivoa.net/xml/VOTable/v1.3"' in xml assert 'xsi:schemaLocation="http://www.ivoa.net/xml/VOTable/v1.3 ' in xml assert 'http://www.ivoa.net/xml/VOTable/VOTable-1.4.xsd"' in xml + + +def _squash_xml(data): + """ + Utility squashing XML fragment to easier their comparison + This function is only used in the test module. It was more convenient for comparing the xml. + """ + return data.replace(" ", "").replace("\n", "").replace('"', "").replace("'", "") + + +def test_mivot_constructor(): + """ + Construct a MIVOT block with wrong tag to test the expected exception + """ + with pytest.raises(ValueError, match="not well-formed"): + MivotBlock( + """ + + Unit test mivot block1 + + + + """ + ) + + +def test_mivot_readout(): + """ + Test the MIVOT block extraction from a file against a reference block stored in data + """ + votable = parse(get_pkg_data_filename("data/mivot_annotated_table.xml")) + + ref_data = "" + for resource in votable.resources: + with open( + get_pkg_data_filename("data/mivot_block_custom_datatype.xml") + ) as reference: + ref_data = reference.read() + assert _squash_xml(ref_data) == _squash_xml(resource.mivot_block.content) + assert len(resource.tables) == 1 + + +def test_mivot_write(): + """ + Build a VOTable, put a MIVOT block in the first resource, checks it can be retrieved + as well as the following table + """ + vot = tree + mivot_block = MivotBlock( + """ + + + Unit test mivot block1 + + + + + """ + ) + vtf = vot.VOTableFile() + mivot_resource = Resource() + mivot_resource.type = "meta" + mivot_resource.mivot_block = mivot_block + # pack the meta resource in a top level resource + r1 = vot.Resource() + r1.type = "results" + r1.resources.append(mivot_resource) + vtf.resources.append(r1) + # Push the VOTable in an IOSTream (emulates a disk saving) + buff = io.BytesIO() + vtf.to_xml(buff) + + # Read the IOStream (emulates a disk readout) + buff.seek(0) + vtf2 = parse(buff) + assert len(vtf2.resources) == 1 + for resource in vtf2.resources: + assert _squash_xml(mivot_block.content) == _squash_xml( + resource.mivot_block.content + ) + assert len(resource.tables) == 0 + + +def test_mivot_write_after_table(): + """ + Build a VOTable, put a MIVOT block and a table in the first resource, checks it can be retrieved + as well as the following table + """ + vot = tree + mivot_block = MivotBlock( + """ + + Unit test mivot block1 + + + + """ + ) + vtf = vot.VOTableFile() + mivot_resource = Resource() + mivot_resource.type = "meta" + mivot_resource.mivot_block = mivot_block + # pack the meta resource in a top level resource + r1 = vot.Resource() + r1.type = "results" + i1 = vot.Info(name="test_name", value="test_value") + r1.infos.append(i1) + r1.resources.append(mivot_resource) + t1 = vot.Table(vtf) + t1.name = "t1" + r1.tables.append(t1) + vtf.resources.append(r1) + # Push the VOTable in an IOSTream (emulates a disk saving) + buff = io.BytesIO() + vtf.to_xml(buff) + + # Read the IOStream (emulates a disk readout) + buff.seek(0) + vtf2 = parse(buff) + assert len(vtf2.resources) == 1 + for resource in vtf2.resources: + assert _squash_xml(mivot_block.content) == _squash_xml( + resource.mivot_block.content + ) + assert len(resource.tables) == 1 + + +def test_write_no_mivot(): + """ + Build a VOTable, put an empty MIVOT block in the first resource, checks it can be retrieved + as well as the following table + """ + vot = tree + vtf = vot.VOTableFile() + mivot_resource = Resource() + mivot_resource.type = "meta" + # pack the meta resource in a top level resource + r1 = vot.Resource() + r1.type = "results" + r1.resources.append(mivot_resource) + t1 = vot.Table(vtf) + t1.name = "t1" + r1.tables.append(t1) + vtf.resources.append(r1) + # Push the VOTable in an IOSTream (emulates a disk saving) + buff = io.BytesIO() + vtf.to_xml(buff) + + # Read the IOStream (emulates a disk readout) + buff.seek(0) + vtf2 = parse(buff) + assert len(vtf2.resources) == 1 + for resource in vtf2.resources: + assert ( + _squash_xml(resource.mivot_block.content) + == "NoMivotblock" + ) + assert len(resource.tables) == 1 + + +def test_mivot_write_after_resource(): + """ + Build a VOTable, put a MIVOT block in the first resource after another meta resource, + checks it can be retrieved as well as the following table + """ + vot = tree + mivot_block = MivotBlock( + """ + + Unit test mivot block1 + + + + """ + ) + vtf = vot.VOTableFile() + mivot_resource = Resource() + mivot_resource.type = "meta" + mivot_resource.mivot_block = mivot_block + # pack the meta resource in a top level resource + r1 = vot.Resource() + r1.type = "results" + i1 = vot.Info(name="test_name", value="test_value") + r1.infos.append(i1) + meta_resource = Resource() + meta_resource.type = "meta" + r1.resources.append(meta_resource) + r1.resources.append(mivot_resource) + t1 = vot.Table(vtf) + t1.name = "t1" + r1.tables.append(t1) + vtf.resources.append(r1) + # Push the VOTable in an IOSTream (emulates a disk saving) + buff = io.BytesIO() + vtf.to_xml(buff) + + # Read the IOStream (emulates a disk readout) + buff.seek(0) + vtf2 = parse(buff) + assert len(vtf2.resources) == 1 + for resource in vtf2.resources: + assert _squash_xml(mivot_block.content) == _squash_xml( + resource.mivot_block.content + ) + assert len(resource.tables) == 1 + + +def test_mivot_forbidden_write(): + """ + Build a meta resource containing a MIVOT block, + build the dummy MIVOT block first. + """ + mivot_block = MivotBlock( + """ + + Unit test mivot block1 + + + """ + ) + # package the MIVOT block in the resource + mivot_resource = Resource() + mivot_resource.type = "results" + + with pytest.raises(E26): + # A MIVOT block must be with "type=meta" + mivot_resource.mivot_block = mivot_block + + +def test_mivot_order(tmp_path): + """ + Build a VOTable with 2 resources containing MivotBlock, parse it, and write it in a file. + Then compare it with another file to see if the order of the elements in a resource is respected, + in particular the MivotBlock which should be before the tables. + """ + vot = tree + mivot_block = MivotBlock( + """ + + + """ + ) + vtf = vot.VOTableFile() + + mivot_resource = Resource() + mivot_resource.type = "meta" + mivot_resource.mivot_block = mivot_block + + mivot_resource2 = Resource() + mivot_resource2.type = "meta" + mivot_resource2.mivot_block = mivot_block + + # R1 : 2 mivot_block, 2 tables, 1 description, 1 info, 1 CooSys + r1 = vot.Resource() + r1.type = "results" + + t1 = vot.Table(vtf) + t1.name = "t1" + t2 = vot.Table(vtf) + t2.name = "t2" + + r1.tables.append(t1) + r1.tables.append(t2) + + r1.resources.append(mivot_resource) + r1.resources.append(mivot_resource2) + + cs = vot.CooSys(ID="_XYZ", system="ICRS") + r1.coordinate_systems.append(cs) + i1 = vot.Info(name="test_name", value="test_value") + r1.infos.append(i1) + + vtf.resources.append(r1) + + # R2 : 1 resource "results", 1 mivot_block and 1 table + r2 = vot.Resource() + r2.type = "results" + + r3 = vot.Resource() + r3.type = "results" + + t3 = vot.Table(vtf) + t3.name = "t3" + r2.tables.append(t3) + r2.resources.append(mivot_resource) + r2.resources.append(r3) + + vtf.resources.append(r2) + + # Push the VOTable in an IOSTream (emulates a disk saving) + buff = io.BytesIO() + vtf.to_xml(buff) + + # Read the IOStream (emulates a disk readout) + buff.seek(0) + vtf2 = parse(buff) + + vpath = get_pkg_data_filename("data/test.order.xml") + vpath_out = str(tmp_path / "test.order.out.xml") + vtf2.to_xml(vpath_out) + + # We want to remove the xml header from the VOTable + with open(vpath_out) as file: + lines = file.readlines() + # The xml header is on 2 lines (line 2 and 3) + del lines[1] + del lines[1] + + with open(vpath_out, "w") as file: + file.writelines(lines) + + assert filecmp.cmp(vpath, vpath_out) diff --git a/astropy/io/votable/tree.py b/astropy/io/votable/tree.py index b8ec93f7cfb..6edaf98e040 100644 --- a/astropy/io/votable/tree.py +++ b/astropy/io/votable/tree.py @@ -19,6 +19,7 @@ from astropy.io import fits from astropy.utils.collections import HomogeneousList from astropy.utils.exceptions import AstropyDeprecationWarning +from astropy.utils.xml import iterparser from astropy.utils.xml.writer import XMLWriter from . import converters, util, xmlutil @@ -41,6 +42,7 @@ E22, E23, E25, + E26, W06, W07, W08, @@ -106,6 +108,7 @@ "Resource", "VOTableFile", "Element", + "MivotBlock", ] @@ -3373,6 +3376,168 @@ def iter_info(self): yield from self.infos +class MivotBlock(Element): + """ + MIVOT Block holder: + Processing VO model views on data is out of the scope of Astropy. + This is why the only VOmodel-related feature implemented here the + extraction or the writing of a mapping block from/to a VOTable + There is no syntax validation other than the allowed tag names. + The mapping block is handled as a correctly indented XML string + which is meant to be parsed by the calling API (e.g., PyVO). + + The constructor takes "content" as a parameter, it is the string + serialization of the MIVOT block. + If it is None, the instance is meant to be set by the Resource parser. + Orherwise, the parameter value is parsed to make sure it matches + the MIVOT XML structure. + + """ + + def __init__(self, content=None): + if content is not None: + self._content = content.strip() + self.check_content_format() + else: + self._content = "" + self._indent_level = 0 + self._on_error = False + + def __str__(self): + return self._content + + def _add_statement(self, start, tag, data, config, pos): + """ + Convert the tag as a string and append it to the mapping + block string with the correct indentation level. + The signature is the same as for all _add_* methods of the parser. + """ + if self._on_error is True: + return + # The first mapping tag () is consumed by the host RESOURCE + # To check that the content is a mapping block. This cannot be done here + # because that RESOURCE might have another content + if self._content == "": + self._content = '\n' + self._indent_level += 1 + + ele_content = "" + if start: + element = "<" + tag + for k, v in data.items(): + element += f" {k}='{v}'" + element += ">\n" + else: + if data: + ele_content = f"{data}\n" + element = f"\n" + + if start is False: + self._indent_level -= 1 + # The content is formatted on the fly: not mandatory but cool for debugging + indent = "".join(" " for _ in range(2 * self._indent_level)) + if ele_content: + self._content += indent + " " + ele_content + self._content += indent + element + if start is True: + self._indent_level += 1 + + def _unknown_mapping_tag(self, start, tag, data, config, pos): + """ + In case of unexpected tag, the parsing stops and the mapping block + is set with a REPORT tag telling what went wrong. + The signature si that same as for all _add_* methods of the parser. + """ + self._content = ( + f'\n ' + f'Unknown mivot block statement: {tag}\n' + ) + self._on_error = True + warn_or_raise(W10, W10, tag, config, pos=pos) + + @property + def content(self): + """ + The XML mapping block serialized as string. + If there is not mapping block, an empty block is returned in order to + prevent client code to deal with None blocks. + """ + if self._content == "": + self._content = ( + '\n ' + 'No Mivot block\n\n' + ) + return self._content + + def parse(self, votable, iterator, config): + """ + Regular parser similar to others VOTable components. + """ + model_mapping_mapping = { + "VODML": self._add_statement, + "GLOBALS": self._add_statement, + "REPORT": self._add_statement, + "MODEL": self._add_statement, + "TEMPLATES": self._add_statement, + "COLLECTION": self._add_statement, + "INSTANCE": self._add_statement, + "ATTRIBUTE": self._add_statement, + "REFERENCE": self._add_statement, + "JOIN": self._add_statement, + "WHERE": self._add_statement, + "PRIMARY_KEY": self._add_statement, + "FOREIGN_KEY": self._add_statement, + } + for start, tag, data, pos in iterator: + model_mapping_mapping.get(tag, self._unknown_mapping_tag)( + start, tag, data, config, pos + ) + if start is False and tag == "VODML": + break + + return self + + def to_xml(self, w): + """ + Tells the writer to insert the MIVOT block in its output stream. + """ + w.string_element(self._content) + + def check_content_format(self): + """ + Check if the content is on xml format by building a VOTable, + putting a MIVOT block in the first resource and trying to parse the VOTable. + """ + if not self._content.startswith("<"): + vo_raise(E26) + + in_memory_votable = VOTableFile() + mivot_resource = Resource() + mivot_resource.type = "meta" + mivot_resource.mivot_block = self + # pack the meta resource in a top level resource + result_resource = Resource() + result_resource.type = "results" + result_resource.resources.append(mivot_resource) + data_table = Table(in_memory_votable) + data_table.name = "t1" + result_resource.tables.append(data_table) + in_memory_votable.resources.append(result_resource) + + # Push the VOTable in an IOSTream (emulates a disk saving) + buff = io.BytesIO() + in_memory_votable.to_xml(buff) + + # Read the IOStream (emulates a disk readout) + buff.seek(0) + config = {} + + with iterparser.get_xml_iterator( + buff, _debug_python_based_parser=None + ) as iterator: + return VOTableFile(config=config, pos=(1, 1)).parse(iterator, config) + + class Resource( Element, _IDProperty, _NameProperty, _UtypeProperty, _DescriptionProperty ): @@ -3416,6 +3581,7 @@ def __init__( self._tables = HomogeneousList(Table) self._resources = HomogeneousList(Resource) + self._mivot_block = MivotBlock() warn_unknown_attrs("RESOURCE", kwargs.keys(), config, pos) def __repr__(self): @@ -3444,6 +3610,26 @@ def type(self, type): vo_raise(E18, type, self._config, self._pos) self._type = type + @property + def mivot_block(self): + """ + Returns the MIVOT block instance. + If the host resource is of type results, it is taken from the first + child resource with a MIVOT block, if any. + Otherwise, it is taken from the host resource. + """ + if self.type == "results": + for resource in self.resources: + if str(resource._mivot_block).strip() != "": + return resource._mivot_block + return self._mivot_block + + @mivot_block.setter + def mivot_block(self, mivot_block): + if self.type == "results": + vo_raise(E26) + self._mivot_block = mivot_block + @property def extra_attributes(self): """Dictionary of extra attributes of the RESOURCE_ element. @@ -3575,7 +3761,11 @@ def parse(self, votable, iterator, config): } for start, tag, data, pos in iterator: - if start: + # If the resource content starts with VODML, + # the parsing is delegated to the MIVOT parser + if tag == "VODML": + self._mivot_block.parse(votable, iterator, config) + elif start: tag_mapping.get(tag, self._add_unknown_tag)( iterator, tag, data, config, pos ) @@ -3596,18 +3786,30 @@ def to_xml(self, w, **kwargs): with w.tag("RESOURCE", attrib=attrs): if self.description is not None: w.element("DESCRIPTION", self.description, wrap=True) + if self.mivot_block is not None and self.type == "meta": + self.mivot_block.to_xml(w) for element_set in ( self.coordinate_systems, self.time_systems, self.params, self.infos, self.links, - self.tables, - self.resources, ): for element in element_set: element.to_xml(w, **kwargs) + # The mivot_block should be before the table + for elm in self.resources: + if elm.type == "meta" and elm.mivot_block is not None: + elm.to_xml(w, **kwargs) + + for elm in self.tables: + elm.to_xml(w, **kwargs) + + for elm in self.resources: + if elm.type != "meta": + elm.to_xml(w, **kwargs) + def iter_tables(self): """ Recursively iterates over all tables in the resource and diff --git a/astropy/utils/xml/writer.py b/astropy/utils/xml/writer.py index 657f92c057c..d9c36041c54 100644 --- a/astropy/utils/xml/writer.py +++ b/astropy/utils/xml/writer.py @@ -304,6 +304,15 @@ def element(self, tag, text=None, wrap=False, attrib={}, **extra): self.data(text) self.end(indent=False, wrap=wrap) + def string_element(self, xml_string): + """ + Reformat the indentation in the XML to insert the MIVOT block. + """ + self._flush() + indent = self.get_indentation_spaces() + str_to_write = indent + xml_string.replace("\n", f"\n{indent}").strip() + "\n" + self.write(str_to_write) + def flush(self): pass # replaced by the constructor diff --git a/docs/changes/io.votable/15390.feature.rst b/docs/changes/io.votable/15390.feature.rst new file mode 100644 index 00000000000..90eacb4484d --- /dev/null +++ b/docs/changes/io.votable/15390.feature.rst @@ -0,0 +1,2 @@ +Added MIVOT feature through the ``MivotBlock`` class +that allows model annotations reading and writing in VOTable. diff --git a/docs/io/votable/index.rst b/docs/io/votable/index.rst index 7d1d1999ccc..a06b245f114 100644 --- a/docs/io/votable/index.rst +++ b/docs/io/votable/index.rst @@ -465,6 +465,135 @@ record array must be resized repeatedly during load. .. _nrows: http://www.ivoa.net/documents/REC/VOTable/VOTable-20040811.html#ToC10 +.. _votable_mivot: + +Reading and writing VO model annotations +======================================== + +Introduction +------------ +Model Instances in VOTables (`MIVOT `_) +defines a syntax to map VOTable data to any model serialised in VO-DML (Virtual Observatory Data Modeling Language). +This annotation schema operates as a bridge between data and the models. It associates both column/param metadata and data +from the VOTable to the data model elements (class, attributes, types, etc.). It also brings up VOTable data or +metadata that were possibly missing in the table, e.g., coordinate system description, or curation tracing. +The data model elements are grouped in an independent annotation block complying with the MIVOT XML schema which +is added as an extra resource above the table element. +The MIVOT syntax allows to describe a data structure as a hierarchy of classes. +It is also able to represent relations and compositions between them. It can moreover build up data model objects by +aggregating instances from different tables of the VOTable. + +Astropy implementation +---------------------- +The purpose of Astropy is not to process VO annotations. +It is just to allow related packages to get and set MIVOT blocks from/into VOTables. +For this reason, in this implementation MIVOT annotations are both imported and exported as strings. +The current implementation prevents client code from injecting into VOTables strings +that are not MIVOT serializations. + +MivotBlock implementation: + +- MIVOT blocks are handled by the :class:`astropy.io.votable.tree.MivotBlock` class. +- A MivotBlock instance can only be carried by a resource with "type=meta". +- This instance holds the XML mapping block as a string. +- MivotBlock objects are instanced by the Resource parser. +- The MivotBlock class has its own logic that operates both parsing and IO functionalities. + +Example +^^^^^^^ + +.. code-block:: xml + + + + + + ... + + +
+ .... +
+
+
+ +Reading a VOTable containing a MIVOT block +------------------------------------------ + +To read in a VOTable file containing or not a MIVOT Resource, pass a file path to`~astropy.io.votable.parse`: + +.. code-block:: python + + >>> from astropy.io.votable import parse + >>> from astropy.utils.data import get_pkg_data_filename + >>> votable = parse(get_pkg_data_filename("data/test.order.xml", package="astropy.io.votable.tests")) + + + + + + +The parse function will call the MIVOT parser if it detects a MIVOT block. + +Building a Resource containing a MIVOT block +-------------------------------------------- + +Construct the MIVOT block by passing the XML block as a parameter: + +.. code-block:: python + + >>> from astropy.io.votable import tree + >>> from astropy.io.votable.tree import MivotBlock, Resource, VOTableFile + >>> mivot_block = MivotBlock(""" + + Unit test mapping block + + + """) + +Build a new resource: + +.. code-block:: python + + >>> mivot_resource = Resource() + +Give it the type meta: + +.. code-block:: python + + >>> mivot_resource.type = "meta" + +Then add it the MIVOT block: + +.. code-block:: python + + >>> mivot_resource.mivot_block = mivot_block + +Now you have a MIVOT resource that you can add to an object Resource creating a new Resource: + +.. code-block:: python + + >>> votable = VOTableFile() + >>> r1 = Resource() + >>> r1.type = "results" + >>> r1.resources.append(mivot_resource) + +You can add an `astropy.io.votable.tree.Table` to the resource: + +.. code-block:: python + + >>> table = tree.Table(votable) + >>> r1.tables.append(t1) + >>> votable.resources.append(r1) + >>> for resource in votable.resources: + ... print(resource.mivot_block.content) + + Unit test mapping block + + + + See Also ======== @@ -480,6 +609,9 @@ See Also - `VOTable Format Definition Version 1.4 `_ +- `MIVOT Recommendation Version 1.0 + `_ + .. note that if this section gets too long, it should be moved to a separate doc page - see the top of performance.inc.rst for the instructions on how to do that diff --git a/docs/whatsnew/6.0.rst b/docs/whatsnew/6.0.rst index d58c989e6a2..542c17afaef 100644 --- a/docs/whatsnew/6.0.rst +++ b/docs/whatsnew/6.0.rst @@ -17,6 +17,7 @@ In particular, this release includes: * :ref:`whatsnew-6.0-broadcasting-frame-attributes` * :ref:`whatsnew-6.0-cosmology-latex-export` * :ref:`whatsnew-6.0-iers-data` +* :ref:`whatsnew-6.0-model-annotation-in-votable` In addition to these major changes, Astropy v6.0 includes a large number of smaller improvements and bug fixes, which are described in the :ref:`changelog`. @@ -235,6 +236,23 @@ download the package manually and transfer it to a computer that has no public internet connection. +.. _whatsnew-6.0-model-annotation-in-votable: + +Reading and writing VO model annotations +======================================== + +Model Instances in VOTables (`MIVOT `_) +defines a syntax to map VOTable data to any model serialised in VO-DML (Virtual Observatory Data Modeling Language). +The data model elements are grouped in an independent annotation block complying with +the MIVOT XML schema which is added as an extra resource above the table element. +In Astropy, the MIVOT block is implemented as a new component of the Resource element (MivotBlock class). +MivotBlock instances can only be held by resources with "type=meta". +In this new feature, Astropy is able to read and write MIVOT annotations from and within VOTables. +There is no function processing data models, they will be delegated to affiliated packages such as PyVO. + +See :ref:`votable_mivot` for more details. + + Full change log ===============