6fa326d576c97d18a6e50d15998e6c866db5d6a2
[htsworkflow.git] / htsworkflow / util / rdfhelp.py
1 """Helper features for working with librdf
2 """
3 import collections
4 from datetime import datetime
5 from glob import glob
6 from urlparse import urlparse, urlunparse
7 from urllib2 import urlopen
8 import logging
9 import os
10 import sys
11 import types
12
13 import lxml.html
14 import lxml.html.clean
15 import RDF
16
17 logger = logging.getLogger(__name__)
18
19 from htsworkflow.util.rdfns import *
20
21 SCHEMAS_URL='http://jumpgate.caltech.edu/phony/schemas'
22 INFERENCE_URL='http://jumpgate.caltech.edu/phony/inference'
23
24 ISOFORMAT_MS = "%Y-%m-%dT%H:%M:%S.%f"
25 ISOFORMAT_SHORT = "%Y-%m-%dT%H:%M:%S"
26
27 def sparql_query(model, query_filename, output_format='text'):
28     """Execute sparql query from file
29     """
30     logger.info("Opening: %s" % (query_filename,))
31     query_body = open(query_filename, 'r').read()
32     query = RDF.SPARQLQuery(query_body)
33     results = query.execute(model)
34     if output_format == 'html':
35         html_query_results(results)
36     else:
37         display_query_results(results)
38
39
40 def display_query_results(results):
41     """A very simple display of sparql query results showing name value pairs
42     """
43     for row in results:
44         for k, v in row.items()[::-1]:
45             print "{0}: {1}".format(k, v)
46         print
47
48 def html_query_results(result_stream):
49     from django.conf import settings
50     from django.template import Context, loader
51
52     # I did this because I couldn't figure out how to
53     # get simplify_rdf into the django template as a filter
54     class Simplified(object):
55         def __init__(self, value):
56             self.simple = simplify_rdf(value)
57             if value.is_resource():
58                 self.url = value
59             else:
60                 self.url = None
61
62     template = loader.get_template('rdf_report.html')
63     results = []
64     for row in result_stream:
65         new_row = collections.OrderedDict()
66         row_urls = []
67         for k,v in row.items():
68             new_row[k] = Simplified(v)
69         results.append(new_row)
70     context = Context({'results': results,})
71     print template.render(context)
72
73 def blankOrUri(value=None):
74     """Return a blank node for None or a resource node for strings.
75     """
76     node = None
77     if value is None:
78         node = RDF.Node()
79     elif type(value) in types.StringTypes:
80         node = RDF.Node(uri_string=value)
81     elif isinstance(value, RDF.Node):
82         node = value
83
84     return node
85
86
87 def toTypedNode(value):
88     """Convert a python variable to a RDF Node with its closest xsd type
89     """
90     if type(value) == types.BooleanType:
91         value_type = xsdNS['boolean'].uri
92         if value:
93             value = u'1'
94         else:
95             value = u'0'
96     elif type(value) in (types.IntType, types.LongType):
97         value_type = xsdNS['decimal'].uri
98         value = unicode(value)
99     elif type(value) == types.FloatType:
100         value_type = xsdNS['float'].uri
101         value = unicode(value)
102     elif isinstance(value, datetime):
103         value_type = xsdNS['dateTime'].uri
104         if value.microsecond == 0:
105             value = value.strftime(ISOFORMAT_SHORT)
106         else:
107             value = value.strftime(ISOFORMAT_MS)
108     else:
109         value_type = None
110         value = unicode(value)
111
112     if value_type is not None:
113         node = RDF.Node(literal=value, datatype=value_type)
114     else:
115         node = RDF.Node(literal=unicode(value).encode('utf-8'))
116     return node
117
118
119 def fromTypedNode(node):
120     """Convert a typed RDF Node to its closest python equivalent
121     """
122     if node is None:
123         return None
124
125     value_type = get_node_type(node)
126     literal = node.literal_value['string']
127     literal_lower = literal.lower()
128
129     if value_type == 'boolean':
130         if literal_lower in ('1', 'yes', 'true'):
131             return True
132         elif literal_lower in ('0', 'no', 'false'):
133             return False
134         else:
135             raise ValueError("Unrecognized boolean %s" % (literal,))
136     elif value_type == 'integer':
137         return int(literal)
138     elif value_type == 'decimal' and literal.find('.') == -1:
139         return int(literal)
140     elif value_type in ('decimal', 'float', 'double'):
141         return float(literal)
142     elif value_type in ('string'):
143         return literal
144     elif value_type in ('dateTime'):
145         try:
146             return datetime.strptime(literal, ISOFORMAT_MS)
147         except ValueError, _:
148             return datetime.strptime(literal, ISOFORMAT_SHORT)
149     return literal
150
151
152 def get_node_type(node):
153     """Return just the base name of a XSD datatype:
154     e.g. http://www.w3.org/2001/XMLSchema#integer -> integer
155     """
156     # chop off xml schema declaration
157     value_type = node.literal_value['datatype']
158     if value_type is None:
159         return "string"
160     else:
161         value_type = str(value_type)
162         return value_type.replace(str(xsdNS[''].uri), '')
163
164
165 def simplify_rdf(value):
166     """Return a short name for a RDF object
167     e.g. The last part of a URI or an untyped string.
168     """
169     if isinstance(value, RDF.Node):
170         if value.is_resource():
171             name = simplify_uri(str(value.uri))
172         elif value.is_blank():
173             name = '<BLANK>'
174         else:
175             name = value.literal_value['string']
176     elif isinstance(value, RDF.Uri):
177         name = split_uri(str(value))
178     else:
179         name = value
180     return str(name)
181
182
183 def simplify_uri(uri):
184     """Split off the end of a uri
185
186     >>> simplify_uri('http://asdf.org/foo/bar')
187     'bar'
188     >>> simplify_uri('http://asdf.org/foo/bar#bleem')
189     'bleem'
190     >>> simplify_uri('http://asdf.org/foo/bar/')
191     'bar'
192     >>> simplify_uri('http://asdf.org/foo/bar?was=foo')
193     'was=foo'
194     """
195     if isinstance(uri, RDF.Node):
196         if uri.is_resource():
197             uri = uri.uri
198         else:
199             raise ValueError("Can't simplify an RDF literal")
200     if isinstance(uri, RDF.Uri):
201         uri = str(uri)
202
203     parsed = urlparse(uri)
204     if len(parsed.query) > 0:
205         return parsed.query
206     elif len(parsed.fragment) > 0:
207         return parsed.fragment
208     elif len(parsed.path) > 0:
209         for element in reversed(parsed.path.split('/')):
210             if len(element) > 0:
211                 return element
212     raise ValueError("Unable to simplify %s" % (uri,))
213
214 def stripNamespace(namespace, term):
215     """Remove the namespace portion of a term
216
217     returns None if they aren't in common
218     """
219     if isinstance(term, RDF.Node):
220         if  term.is_resource():
221             term = term.uri
222         else:
223             raise ValueError("This works on resources")
224     elif not isinstance(term, RDF.Uri):
225         raise ValueError("This works on resources")
226     term_s = str(term)
227     if not term_s.startswith(namespace._prefix):
228         return None
229     return term_s.replace(namespace._prefix, "")
230
231
232 def get_model(model_name=None, directory=None):
233     if directory is None:
234         directory = os.getcwd()
235
236     if model_name is None:
237         storage = RDF.MemoryStorage(options_string="contexts='yes'")
238         logger.info("Using RDF Memory model")
239     else:
240         options = "contexts='yes',hash-type='bdb',dir='{0}'".format(directory)
241         storage = RDF.HashStorage(model_name,
242                       options=options)
243         logger.info("Using {0} with options {1}".format(model_name, options))
244     model = RDF.Model(storage)
245     return model
246
247
248 def load_into_model(model, parser_name, path, ns=None):
249     if type(ns) in types.StringTypes:
250         ns = RDF.Uri(ns)
251
252     if isinstance(path, RDF.Node):
253         if path.is_resource():
254             path = str(path.uri)
255         else:
256             raise ValueError("url to load can't be a RDF literal")
257
258     url_parts = list(urlparse(path))
259     if len(url_parts[0]) == 0 or url_parts[0] == 'file':
260         url_parts[0] = 'file'
261         url_parts[2] = os.path.abspath(url_parts[2])
262     if parser_name is None or parser_name == 'guess':
263         parser_name = guess_parser_by_extension(path)
264     url = urlunparse(url_parts)
265     logger.info("Opening {0} with parser {1}".format(url, parser_name))
266
267     rdf_parser = RDF.Parser(name=parser_name)
268
269     statements = []
270     retries = 3
271     while retries > 0:
272         try:
273             retries -= 1
274             statements = rdf_parser.parse_as_stream(url, ns)
275             retries = 0
276         except RDF.RedlandError, e:
277             errmsg = "RDF.RedlandError: {0} {1} tries remaining"
278             logger.error(errmsg.format(str(e), retries))
279
280     for s in statements:
281         conditionally_add_statement(model, s, ns)
282
283 def load_string_into_model(model, parser_name, data, ns=None):
284     ns = fixup_namespace(ns)
285     logger.debug("load_string_into_model parser={0}, len={1}".format(
286         parser_name, len(data)))
287     rdf_parser = RDF.Parser(name=parser_name)
288
289     for s in rdf_parser.parse_string_as_stream(data, ns):
290         conditionally_add_statement(model, s, ns)
291
292
293 def fixup_namespace(ns):
294     if ns is None:
295         ns = RDF.Uri("http://localhost/")
296     elif type(ns) in types.StringTypes:
297         ns = RDF.Uri(ns)
298     elif not(isinstance(ns, RDF.Uri)):
299         errmsg = "Namespace should be string or uri not {0}"
300         raise ValueError(errmsg.format(str(type(ns))))
301     return ns
302
303
304 def conditionally_add_statement(model, s, ns):
305     imports = owlNS['imports']
306     if s.predicate == imports:
307         obj = str(s.object)
308         logger.info("Importing %s" % (obj,))
309         load_into_model(model, None, obj, ns)
310     if s.object.is_literal():
311         value_type = get_node_type(s.object)
312         if value_type == 'string':
313             s.object = sanitize_literal(s.object)
314     model.add_statement(s)
315
316
317 def add_default_schemas(model, schema_path=None):
318     """Add default schemas to a model
319     Looks for turtle files in either htsworkflow/util/schemas
320     or in the list of directories provided in schema_path
321     """
322
323     if schema_path is None:
324         path, _ = os.path.split(__file__)
325         schema_path = [os.path.join(path, 'schemas')]
326     elif type(schema_path) in types.StringTypes:
327         schema_path = [schema_path]
328
329     for p in schema_path:
330         for f in glob(os.path.join(p, '*.turtle')):
331             add_schema(model, f)
332
333 def add_schema(model, filename):
334     """Add a schema to a model.
335
336     Main difference from 'load_into_model' is it tags it with
337     a RDFlib context so I can remove them later.
338     """
339     parser = RDF.Parser(name='turtle')
340     context = RDF.Node(RDF.Uri(SCHEMAS_URL))
341     url = 'file://' + filename
342     for s in parser.parse_as_stream(url):
343         try:
344             model.append(s, context)
345         except RDF.RedlandError as e:
346             logger.error("%s with %s", str(e), str(s))
347
348
349 def remove_schemas(model):
350     """Remove statements labeled with our schema context"""
351     context = RDF.Node(RDF.Uri(SCHEMAS_URL))
352     model.context_remove_statements(context)
353
354
355 def sanitize_literal(node):
356     """Clean up a literal string
357     """
358     if not isinstance(node, RDF.Node):
359         raise ValueError("sanitize_literal only works on RDF.Nodes")
360
361     s = node.literal_value['string']
362     if len(s) > 0:
363         element = lxml.html.fromstring(s)
364         cleaner = lxml.html.clean.Cleaner(page_structure=False)
365         element = cleaner.clean_html(element)
366         text = lxml.html.tostring(element)
367         p_len = 3
368         slash_p_len = 4
369
370         args = {'literal': text[p_len:-slash_p_len]}
371     else:
372         args = {'literal': ''}
373     datatype = node.literal_value['datatype']
374     if datatype is not None:
375         args['datatype'] = datatype
376     language = node.literal_value['language']
377     if language is not None:
378         args['language'] = language
379     return RDF.Node(**args)
380
381
382 def guess_parser(content_type, pathname):
383     if content_type in ('application/rdf+xml',):
384         return 'rdfxml'
385     elif content_type in ('application/x-turtle',):
386         return 'turtle'
387     elif content_type in ('text/html',):
388         return 'rdfa'
389     elif content_type is None or content_type in ('text/plain',):
390         return guess_parser_by_extension(pathname)
391
392 def guess_parser_by_extension(pathname):
393     _, ext = os.path.splitext(pathname)
394     if ext in ('.xml', '.rdf'):
395         return 'rdfxml'
396     elif ext in ('.html',):
397         return 'rdfa'
398     elif ext in ('.turtle',):
399         return 'turtle'
400     return 'guess'
401
402 def get_serializer(name='turtle'):
403     """Return a serializer with our standard prefixes loaded
404     """
405     writer = RDF.Serializer(name=name)
406     # really standard stuff
407     writer.set_namespace('rdf', rdfNS._prefix)
408     writer.set_namespace('rdfs', rdfsNS._prefix)
409     writer.set_namespace('owl', owlNS._prefix)
410     writer.set_namespace('dc', dcNS._prefix)
411     writer.set_namespace('xml', xmlNS._prefix)
412     writer.set_namespace('xsd', xsdNS._prefix)
413     writer.set_namespace('vs', vsNS._prefix)
414     writer.set_namespace('wot', wotNS._prefix)
415
416     # should these be here, kind of specific to an application
417     writer.set_namespace('libraryOntology', libraryOntology._prefix)
418     writer.set_namespace('ucscSubmission', submissionOntology._prefix)
419     writer.set_namespace('ucscDaf', dafTermOntology._prefix)
420     return writer
421
422
423 def dump_model(model, destination=None):
424     if destination is None:
425         destination = sys.stdout
426     serializer = get_serializer()
427     destination.write(serializer.serialize_model_to_string(model))
428     destination.write(os.linesep)