"""
Parsing VO-DML files and (perhaps one day) validating against the
rules obtained in this way.
Validation is something we expect to do only fairly rarely, so none of
this code is expected to be efficient.
"""
#c Copyright 2008-2023, the GAVO project <gavo@ari.uni-heidelberg.de>
#c
#c This program is free software, covered by the GNU GPL. See the
#c COPYING file in the source distribution.
import functools
from lxml import etree
from gavo import base
from gavo.votable import V
KNOWN_MODELS = {
# maps the canonical prefix to the file name within resources/dm
"ivoa": "IVOA.vo-dml.xml",
"meas": "meas.vo-dml.xml",
"coords": "coords.vo-dml.xml",
"dachstoy": "dachstoy.vo-dml.xml",
"geojson": "geojson.vo-dml.xml",
"phot": "phot.vo-dml.xml",
}
[docs]def openModelFile(prefix):
"""returns an open file for the VO-DML file corresponding to prefix.
This will raise a NotFoundError for an unknown prefix.
"""
try:
fName = KNOWN_MODELS[prefix]
except KeyError:
raise base.NotFoundError(prefix, "VO-DML file for prefix",
"data models known to DaCHS", hint="This can happen if there"
" are new data models around or if data providers have defined"
" custom data models. If this error was fatal during VOTable"
" processing, please report it as an error; bad data model"
" annotation should not be fatal in DaCHS.")
return base.openDistFile("dm/"+fName, "rb")
[docs]class Model(object):
"""a vo-dml model.
These are usually constructed using the fromPrefix constructor,
which uses a built-in mapping from well-known prefix to VO-DML file
to populate the model.
"""
# non-well-known models can be fed in through fromFile; they will
# be entered here and can then be obtained through fromPrefix
# as long as the don't clash with KNOWN_MODELS.
_modelsReadFromFile = {}
def __init__(self, prefix, dmlTree):
self.prefix = prefix
self.title = self.version = None
self.version = self.uri = None
self.description = None
self.dmlTree = dmlTree
self.__idIndex = None
self._getModelMeta()
[docs] @classmethod
def fromPrefix(cls, prefix):
"""returns a VO-DML model for a well-known prefix.
User code should typically use the getModelFromPrefix function.
"""
if prefix in cls._modelsReadFromFile:
return cls._modelsReadFromFile[prefix]
inF = openModelFile(prefix)
try:
try:
return cls(prefix, etree.parse(inF))
except Exception as ex:
raise base.ui.logOldExc(
base.StructureError("Failure to parse VO-DML for prefix %s: %s"%(
prefix, repr(ex))))
finally:
inF.close()
[docs] @classmethod
def fromFile(cls, src, srcURL="http //not.given/invalid"):
"""returns a VO-DML model from src.
src can either be a file name (interpreted relative to the root
of DaCHS' VO-DML repository) or an open file (which will be closed
as a side effect of this function).
This is intended for documents using non-standard models with custom
prefixes (i.e., not known to DaCHS).
"""
if hasattr(src, "read"):
inF = src
else:
inF = openModelFile(src)
try:
tree = etree.parse(inF)
prefix = tree.find("name").text
res = cls(prefix, tree)
res.uri = srcURL
if prefix not in KNOWN_MODELS:
cls._modelsReadFromFile[prefix] = res
return res
finally:
inF.close()
def _getModelMeta(self):
"""sets some metadata on the model from the parsed VO-DML.
This will fail silently (i.e., the metadata will remain on its
default).
Metadata obtained so far includes: title, version, description,
"""
try:
self.title = self.dmlTree.find("title").text
self.version = self.dmlTree.find("version").text
self.description = self.dmlTree.find("description").text
self.uri = self.dmlTree.find("uri").text
except AttributeError:
# probably the VO-DML file is bad; just fall through to
# non-validatable model.
pass
[docs] @functools.lru_cache(200)
def getByVODMLId(self, vodmlId):
"""returns the element with vodmlId.
This raises a NotFoundError for elements that are not present.
This can be used with or without a prefix. The prefix is just
discarded, though.
Do not pass in unparsed ids; they are used in xpaths.
"""
vodmlId = vodmlId.split(":")[-1]
res = self.dmlTree.xpath(f"//*[vodml-id='{vodmlId}']")
if res:
return res[0]
else:
raise base.NotFoundError(vodmlId, "data model element",
self.prefix+" data model")
def _resolveVODMLId(self, vodmlId):
"""returns an etree element for vodmlId, which may include a prefix.
(in which case we'd probably be looking in a different DM).
This will raise a NotFoundError when the vodmlId points nowhere.
"""
if ":" in vodmlId:
return resolveVODMLId(vodmlId)
else:
return self.getByVODMLElement(vodmlId)
def _makeAttrDict(self, attrNode):
"""returns a dictionary of attribute metadata for an etree attrNode.
"""
res = {}
for child in attrNode:
if child.tag=="vodml-id":
res["vodml-id"] = child.text.strip()
elif child.tag=="description":
res["description"] = child.text.strip()
elif child.tag=="datatype":
res["datatype"] = child[0].text
return res
[docs] @functools.lru_cache(100)
def getType(self, name):
"""returns an etree for a data or object type with the *name* name
within this DM.
Any prefix on name will be discarded without further ado. Don't
pass in anythin unparsed here; we are using xpaths.
"""
name = name.split(":")[-1]
type = self.dmlTree.xpath(f"dataType[name='{name}']")
if type:
return type[0]
type = self.dmlTree.xpath(f"objectType[name='{name}']")
if type:
return type[0]
raise base.NotFoundError(
name, "VO-DML type", self.prefix+" data model")
[docs] def getVOT(self, ctx, instance):
"""returns xmlstan for a VOTable declaration of this DM.
"""
return V.MODEL(name=self.prefix, url=self.uri)
[docs]@functools.lru_cache(30)
def getModelForPrefix(prefix):
"""returns a vodml.Model instance for as well-known VODML prefix.
This caches models for prefixes and thus should usually be used
from user code.
Note that this currently will currently return some stand-in shim
for unknown prefixes. That behaviour will change to become a
NotFoundError exception when there's actually useful data models.
"""
try:
return Model.fromPrefix(prefix)
except base.NotFoundError:
res = Model(prefix, etree.fromstring(
"""<junk><title>DaCHS standin model</title>
<description>This is used by DaCHS during the old west
days of VO DM development. Any annotation using this will
not be interoperable.</description>
<version>invalid</version></junk>"""))
res.uri = "urn:dachsjunk:not-model:"+prefix
return res
[docs]def getAttributeDefinition(qualifiedType, attrName):
"""returns attribute metadata for a type.
qualifiedType is a type name with a prefix, attrName is the attribute's
name (not vodml-id).
"""
prefix, typeName = qualifiedType.split(":")
return getModelForPrefix(prefix).getAttributeMeta(typeName, attrName)
[docs]def resolveVODMLId(vodmlId):
"""returns an etree element corresponding to the prefixed vodmlId.
Of course, this only works if vodmlId has a well-known prefix.
"""
prefix, id = vodmlId.split(":", 1)
return getModelForPrefix(prefix).getByVODMLId(id)