Port rdfhelp to rdflib from redland librdf.
[htsworkflow.git] / htsworkflow / util / rdfhelp.py
index 30d91788c47565174ef12a6133a5093c350409e8..29fdb7c90d44e34d3a32ce10285c1dfcb06094b0 100644 (file)
@@ -13,9 +13,11 @@ import sys
 import types
 from pkg_resources import resource_listdir, resource_string
 
+from rdflib import ConjunctiveGraph, Graph, Literal, BNode, URIRef, Namespace
+from rdflib.namespace import ClosedNamespace
+
 import lxml.html
 import lxml.html.clean
-import RDF
 
 logger = logging.getLogger(__name__)
 
@@ -73,115 +75,28 @@ def html_query_results(result_stream):
     context = Context({'results': results,})
     print(template.render(context))
 
-def blankOrUri(value=None):
-    """Return a blank node for None or a resource node for strings.
-    """
-    node = None
-    if value is None:
-        node = RDF.Node()
-    elif isinstance(value, six.string_types):
-        node = RDF.Node(uri_string=value)
-    elif isinstance(value, RDF.Node):
-        node = value
-
-    return node
-
-
-def toTypedNode(value, language="en"):
-    """Convert a python variable to a RDF Node with its closest xsd type
-    """
-    if isinstance(value, bool):
-        value_type = xsdNS['boolean'].uri
-        if value:
-            value = u'1'
-        else:
-            value = u'0'
-    elif isinstance(value, int):
-        value_type = xsdNS['decimal'].uri
-        value = str(value)
-    elif isinstance(value, float):
-        value_type = xsdNS['float'].uri
-        value = str(value)
-    elif isinstance(value, datetime):
-        value_type = xsdNS['dateTime'].uri
-        if value.microsecond == 0:
-            value = value.strftime(ISOFORMAT_SHORT)
-        else:
-            value = value.strftime(ISOFORMAT_MS)
-    else:
-        value_type = None
-        if six.PY3:
-            value = str(value)
-        else:
-            value = unicode(value).encode('utf-8')
-
-    if value_type is not None:
-        node = RDF.Node(literal=value, datatype=value_type)
-    else:
-        node = RDF.Node(literal=value, language=language)
-    return node
-
-
-def fromTypedNode(node):
-    """Convert a typed RDF Node to its closest python equivalent
-    """
-    if not isinstance(node, RDF.Node):
-        return node
-    if node.is_resource():
-        return node
-
-    value_type = get_node_type(node)
-    literal = node.literal_value['string']
-    literal_lower = literal.lower()
-
-    if value_type == 'boolean':
-        if literal_lower in ('1', 'yes', 'true'):
-            return True
-        elif literal_lower in ('0', 'no', 'false'):
-            return False
-        else:
-            raise ValueError("Unrecognized boolean %s" % (literal,))
-    elif value_type == 'integer':
-        return int(literal)
-    elif value_type == 'decimal' and literal.find('.') == -1:
-        return int(literal)
-    elif value_type in ('decimal', 'float', 'double'):
-        return float(literal)
-    elif value_type in ('string'):
-        return literal
-    elif value_type in ('dateTime'):
-        try:
-            return datetime.strptime(literal, ISOFORMAT_MS)
-        except ValueError:
-            return datetime.strptime(literal, ISOFORMAT_SHORT)
-    return literal
-
 
 def get_node_type(node):
     """Return just the base name of a XSD datatype:
     e.g. http://www.w3.org/2001/XMLSchema#integer -> integer
     """
     # chop off xml schema declaration
-    value_type = node.literal_value['datatype']
+    value_type = node.datatype
     if value_type is None:
         return "string"
     else:
-        value_type = str(value_type)
-        return value_type.replace(str(xsdNS[''].uri), '')
+        return value_type.replace(str(XSD), '').lower()
 
 
 def simplify_rdf(value):
     """Return a short name for a RDF object
     e.g. The last part of a URI or an untyped string.
     """
-    if isinstance(value, RDF.Node):
-        if value.is_resource():
-            name = simplify_uri(str(value.uri))
-        elif value.is_blank():
-            name = '<BLANK>'
-        else:
-            name = value.literal_value['string']
-    elif isinstance(value, RDF.Uri):
+    if isinstance(value, Literal):
+        name = value.value
+    elif isinstance(value, BNode):
+        name = '<BLANK>'
+    elif isinstance(value, URIRef):
         name = split_uri(str(value))
     else:
         name = value
@@ -200,13 +115,10 @@ def simplify_uri(uri):
     >>> simplify_uri('http://asdf.org/foo/bar?was=foo')
     'was=foo'
     """
-    if isinstance(uri, RDF.Node):
-        if uri.is_resource():
-            uri = uri.uri
-        else:
-            raise ValueError("Can't simplify an RDF literal")
-    if isinstance(uri, RDF.Uri):
-        uri = str(uri)
+    if isinstance(uri, Literal) and uri.datatype not in (XSD.anyURI,):
+        raise ValueError("Literal terms must be of URI type")
+
+    uri = str(uri)
 
     parsed = urllib.parse.urlparse(uri)
     if len(parsed.query) > 0:
@@ -224,46 +136,26 @@ def strip_namespace(namespace, term):
 
     returns None if they aren't in common
     """
-    if isinstance(term, RDF.Node):
-        if  term.is_resource():
-            term = term.uri
-        else:
-            raise ValueError("This works on resources")
-    elif not isinstance(term, RDF.Uri):
-        raise ValueError("This works on resources")
-    term_s = str(term)
-    if not term_s.startswith(namespace._prefix):
-        return None
-    return term_s.replace(namespace._prefix, "")
+    if not isinstance(namespace, (URIRef, Namespace, ClosedNamespace)):
+        raise ValueError("Requires a URIRef namespace")
 
+    if isinstance(term, Literal) and term.datatype not in (XSD.anyURI,):
+        raise ValueError("Term literals must be a URI type")
+    elif not isinstance(term, URIRef):
+        raise ValueError("Term must be a URI type")
 
-def get_model(model_name=None, directory=None, use_contexts=True):
-    if directory is None:
-        directory = os.getcwd()
-
-    contexts = 'yes' if use_contexts else 'no'
-
-    if model_name is None:
-        storage = RDF.MemoryStorage(options_string="contexts='{}'".format(contexts))
-        logger.info("Using RDF Memory model")
-    else:
-        options = "contexts='{0}',hash-type='bdb',dir='{1}'".format(contexts, directory)
-        storage = RDF.HashStorage(model_name,
-                      options=options)
-        logger.info("Using {0} with options {1}".format(model_name, options))
-    model = RDF.Model(storage)
-    return model
+    term_s = str(term)
+    if not term_s.startswith(str(namespace)):
+        return None
+    return term_s.replace(str(namespace), "")
 
 
 def load_into_model(model, parser_name, path, ns=None):
     if isinstance(ns, six.string_types):
-        ns = RDF.Uri(ns)
+        ns = URIRef(ns)
 
-    if isinstance(path, RDF.Node):
-        if path.is_resource():
-            path = str(path.uri)
-        else:
-            raise ValueError("url to load can't be a RDF literal")
+    if isinstance(path, URIRef):
+        path = str(path)
 
     url_parts = list(urllib.parse.urlparse(path))
     if len(url_parts[0]) == 0 or url_parts[0] == 'file':
@@ -274,67 +166,40 @@ def load_into_model(model, parser_name, path, ns=None):
     url = urllib.parse.urlunparse(url_parts)
     logger.info("Opening {0} with parser {1}".format(url, parser_name))
 
-    rdf_parser = RDF.Parser(name=parser_name)
-
-    statements = []
-    retries = 3
-    succeeded = False
-    while retries > 0:
-        try:
-            retries -= 1
-            statements = rdf_parser.parse_as_stream(url, ns)
-            retries = 0
-            succeeded = True
-        except RDF.RedlandError as e:
-            errmsg = "RDF.RedlandError: {0} {1} tries remaining"
-            logger.error(errmsg.format(str(e), retries))
+    model.parse(url, format=parser_name, publicID=ns)
 
-    if not succeeded:
-        logger.warn("Unable to download %s", url)
-
-    for s in statements:
-        conditionally_add_statement(model, s, ns)
 
 def load_string_into_model(model, parser_name, data, ns=None):
     ns = fixup_namespace(ns)
     logger.debug("load_string_into_model parser={0}, len={1}".format(
         parser_name, len(data)))
-    rdf_parser = RDF.Parser(name=str(parser_name))
 
-    for s in rdf_parser.parse_string_as_stream(data, ns):
-        conditionally_add_statement(model, s, ns)
+    model.parse(data=data, format=parser_name, publicID=ns)
+    add_imports(model, ns)
 
 
 def fixup_namespace(ns):
     if ns is None:
-        ns = RDF.Uri("http://localhost/")
+        ns = URIRef("http://localhost/")
     elif isinstance(ns, six.string_types):
-        ns = RDF.Uri(ns)
-    elif not(isinstance(ns, RDF.Uri)):
+        ns = URIRef(ns)
+    elif not(isinstance(ns, URIRef)):
         errmsg = "Namespace should be string or uri not {0}"
         raise ValueError(errmsg.format(str(type(ns))))
     return ns
 
 
-def conditionally_add_statement(model, s, ns):
-    imports = owlNS['imports']
-    if s.predicate == imports:
-        obj = str(s.object)
-        logger.info("Importing %s" % (obj,))
-        load_into_model(model, None, obj, ns)
-    if s.object.is_literal():
-        value_type = get_node_type(s.object)
-        if value_type == 'string':
-            s.object = sanitize_literal(s.object)
-    model.add_statement(s)
-
-
+def add_imports(model, ns):
+    for s, p, o in model.triples((None, OWL.imports, None)):
+        if p == OWL.imports:
+            model.remove((s, p, o))
+            load_into_model(model, None, o, ns)
+            
 def add_default_schemas(model, schema_path=None):
     """Add default schemas to a model
     Looks for turtle files in either htsworkflow/util/schemas
     or in the list of directories provided in schema_path
     """
-
     schemas = resource_listdir(__name__, 'schemas')
     for s in schemas:
         schema = resource_string(__name__,  'schemas/' + s)
@@ -361,28 +226,29 @@ def add_schema(model, schema, url):
     Main difference from 'load_into_model' is it tags it with
     a RDFlib context so I can remove them later.
     """
-    parser = RDF.Parser(name='turtle')
-    context = RDF.Node(RDF.Uri(SCHEMAS_URL))
-    for s in parser.parse_string_as_stream(schema, url):
-        try:
-            model.append(s, context)
-        except RDF.RedlandError as e:
-            logger.error("%s with %s", str(e), str(s))
+    if not isinstance(model, ConjunctiveGraph):
+        raise ValueError("Schemas requires a graph that supports quads")
 
+    context = URIRef(SCHEMAS_URL)
+    tmpmodel = Graph()
+    tmpmodel.parse(data=schema, format='turtle', publicID=url)
+    for s, p, o in tmpmodel:
+        model.add((s, p, o, context))
 
 def remove_schemas(model):
     """Remove statements labeled with our schema context"""
-    context = RDF.Node(RDF.Uri(SCHEMAS_URL))
-    model.context_remove_statements(context)
-
+    context = URIRef(SCHEMAS_URL)
+    for quad in model.triples((None, None, None, context)):
+        model.remove(quad)
+        #model.remove_context(context)
 
 def sanitize_literal(node):
     """Clean up a literal string
     """
-    if not isinstance(node, RDF.Node):
-        raise ValueError("sanitize_literal only works on RDF.Nodes")
+    if not isinstance(node, Literal):
+        raise ValueError("sanitize_literal only works on Literals")
 
-    s = node.literal_value['string']
+    s = node.value
     if len(s) > 0:
         element = lxml.html.fromstring(s)
         cleaner = lxml.html.clean.Cleaner(page_structure=False)
@@ -394,16 +260,15 @@ def sanitize_literal(node):
         p_len = 3
         slash_p_len = 4
 
-        args = {'literal': text[p_len:-slash_p_len]}
+        value = text[p_len:-slash_p_len]
     else:
-        args = {'literal': ''}
-    datatype = node.literal_value['datatype']
-    if datatype is not None:
-        args['datatype'] = datatype
-    language = node.literal_value['language']
-    if language is not None:
-        args['language'] = language
-    return RDF.Node(**args)
+        value = ''
+    args = {}
+    if node.datatype is not None:
+        args['datatype'] = node.datatype
+    if node.language is not None:
+        args['lang'] = node.language
+    return Literal(value, **args)
 
 
 def guess_parser(content_type, pathname):
@@ -426,38 +291,35 @@ def guess_parser_by_extension(pathname):
         return 'turtle'
     return 'guess'
 
-def get_serializer(name='turtle'):
+def add_default_namespaces(model):
     """Return a serializer with our standard prefixes loaded
     """
-    writer = RDF.Serializer(name=name)
-    # really standard stuff
-    writer.set_namespace('rdf', rdfNS._prefix)
-    writer.set_namespace('rdfs', rdfsNS._prefix)
-    writer.set_namespace('owl', owlNS._prefix)
-    writer.set_namespace('dc', dcNS._prefix)
-    writer.set_namespace('xml', xmlNS._prefix)
-    writer.set_namespace('xsd', xsdNS._prefix)
-    writer.set_namespace('vs', vsNS._prefix)
-    writer.set_namespace('wot', wotNS._prefix)
+    model.bind('rdf', RDF)
+    model.bind('rdfs', RDFS)
+    model.bind('owl', OWL)
+    model.bind('dc', DC)
+    model.bind('xml', XML)
+    model.bind('xsd', XSD)
+    model.bind('vs', VS)
+    model.bind('wot', WOT)
 
     # should these be here, kind of specific to an application
-    writer.set_namespace('htswlib', libraryOntology._prefix)
-    writer.set_namespace('ucscSubmission', submissionOntology._prefix)
-    writer.set_namespace('ucscDaf', dafTermOntology._prefix)
-    writer.set_namespace('geoSoft', geoSoftNS._prefix)
-    writer.set_namespace('encode3', encode3NS._prefix)
-    return writer
+    model.bind('htswlib', libraryOntology)
+    model.bind('ucscSubmission', submissionOntology)
+    model.bind('ucscDaf', dafTermOntology)
+    model.bind('geoSoft', geoSoftNS)
+    model.bind('encode3', encode3NS)
+    return model
 
 def get_turtle_header():
     """Return a turtle header with our typical namespaces
     """
-    serializer = get_serializer()
     empty = get_model()
-    return serializer.serialize_model_to_string(empty)
+    add_default_namespaces(model)
+    return model.serialize(format='turtle')
 
 def dump_model(model, destination=None):
     if destination is None:
         destination = sys.stdout
-    serializer = get_serializer()
-    destination.write(serializer.serialize_model_to_string(model))
-    destination.write(os.linesep)
+    add_default_namespaces(model)
+    model.serialize(destination, format='turtle')