Skip to content

Commit c03d5cf

Browse files
authored
Merge pull request #400 from mpsonntag/rdf_subclass
RDF Subclassing feature LGTM
2 parents 7a1ca1f + 41d72b0 commit c03d5cf

File tree

5 files changed

+308
-9
lines changed

5 files changed

+308
-9
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ install:
7575
export PIPCMD=pip;
7676
fi;
7777

78-
- $PIPCMD install lxml enum34 pyyaml rdflib
78+
- $PIPCMD install lxml enum34 pyyaml rdflib owlrl requests
7979

8080
script:
8181
- which $PYCMD

appveyor.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ init:
3737
build: false
3838

3939
install:
40-
- python -m pip install lxml enum34 pyyaml rdflib
40+
- python -m pip install lxml enum34 pyyaml rdflib owlrl requests
4141

4242
test_script:
4343
- python --version

odml/tools/rdf_converter.py

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44
"""
55

66
import os
7+
import string
78
import uuid
9+
import warnings
810

911
from io import StringIO
1012
from rdflib import Graph, Literal, URIRef
1113
from rdflib.graph import Seq
12-
from rdflib.namespace import XSD, RDF
14+
from rdflib.namespace import XSD, RDF, RDFS
1315

1416
import yaml
1517

@@ -57,14 +59,32 @@ class RDFWriter(object):
5759
"""
5860
A writer to parse odML files into RDF documents.
5961
62+
Use the 'rdf_subclassing' flag to disable default usage of Section type conversion to
63+
RDF Subclasses.
64+
Provide a custom Section type to RDF Subclass Name mapping dictionary via the
65+
'custom_subclasses' attribute to add custom or overwrite default RDF Subclass mappings.
66+
6067
Usage:
6168
RDFWriter(odml_docs).get_rdf_str('turtle')
6269
RDFWriter(odml_docs).write_file("/output_path", "rdf_format")
70+
71+
RDFWriter(odml_docs, rdf_subclassing=False).write_file("path", "rdf_format")
72+
RDFWriter(odml_docs, custom_subclasses=custom_dict).write_file("path", "rdf_format")
6373
"""
6474

65-
def __init__(self, odml_documents):
75+
def __init__(self, odml_documents, rdf_subclassing=True, custom_subclasses=None):
6676
"""
6777
:param odml_documents: list of odML documents
78+
:param rdf_subclassing: Flag whether Section types should be converted to RDF Subclasses
79+
for enhanced SPARQL queries. Default is 'True'.
80+
:param custom_subclasses: A dict where the keys reference a Section type and the
81+
corresponding values reference an RDF Class Name. When exporting
82+
a Section of a type contained in this dict, the resulting RDF
83+
Instance will be of the corresponding Class and this Class will
84+
be added as a Subclass of RDF Class "odml:Section" to the
85+
RDF document.
86+
Key:value pairs of the "custom_subclasses" dict will overwrite
87+
existing key:value pairs of the default subclassing dict.
6888
"""
6989
if not isinstance(odml_documents, list):
7090
odml_documents = [odml_documents]
@@ -74,7 +94,13 @@ def __init__(self, odml_documents):
7494
self.graph = Graph()
7595
self.graph.bind("odml", ODML_NS)
7696

97+
self.rdf_subclassing = rdf_subclassing
98+
7799
self.section_subclasses = load_rdf_subclasses()
100+
# If a custom Section type to RDF Subclass dict has been provided,
101+
# parse it and update the default section_subclasses dict with the content.
102+
if custom_subclasses and isinstance(custom_subclasses, dict):
103+
self._parse_custom_subclasses(custom_subclasses)
78104

79105
def convert_to_rdf(self):
80106
"""
@@ -221,10 +247,16 @@ def save_section(self, sec, curr_node):
221247

222248
# Add type of current node to the RDF graph
223249
curr_type = fmt.rdf_type
250+
224251
# Handle section subclass types
225-
sub_sec = self._get_section_subclass(sec)
226-
if sub_sec:
227-
curr_type = sub_sec
252+
if self.rdf_subclassing:
253+
sub_sec = self._get_section_subclass(sec)
254+
if sub_sec:
255+
curr_type = sub_sec
256+
self.graph.add((URIRef(fmt.rdf_type), RDF.type, RDFS.Class))
257+
self.graph.add((URIRef(curr_type), RDF.type, RDFS.Class))
258+
self.graph.add((URIRef(curr_type), RDFS.subClassOf, URIRef(fmt.rdf_type)))
259+
228260
self.graph.add((curr_node, RDF.type, URIRef(curr_type)))
229261

230262
for k in fmt.rdf_map_keys:
@@ -294,6 +326,33 @@ class Section.
294326

295327
return None
296328

329+
def _parse_custom_subclasses(self, custom_subclasses):
330+
"""
331+
Parses a provided dictionary of "Section type": "RDF Subclass name"
332+
key value pairs and adds the pairs to the parsers' 'section_subclasses'
333+
default dictionary. Existing key:value pairs will be overwritten
334+
with provided custom key:value pairs and a Warning will be issued.
335+
Dictionary values containing whitespaces will raise a ValueError.
336+
337+
:param custom_subclasses: dictionary of "Section type": "RDF Subclass name" key value pairs.
338+
Values must not contain whitespaces, a ValueError will be raised
339+
otherwise.
340+
"""
341+
342+
# Do not allow any whitespace characters in values
343+
vals = "".join(custom_subclasses.values()).encode()
344+
if vals != vals.translate(None, string.whitespace.encode()):
345+
msg = "Custom RDF Subclass names must not contain any whitespace characters."
346+
raise ValueError(msg)
347+
348+
for k in custom_subclasses:
349+
val = custom_subclasses[k]
350+
if k in self.section_subclasses:
351+
msg = "RDFWriter custom subclasses: Key '%s' already exists. " % k
352+
msg += "Value '%s' replaces default value '%s'." % (val, self.section_subclasses[k])
353+
warnings.warn(msg, stacklevel=2)
354+
self.section_subclasses[k] = val
355+
297356
def __str__(self):
298357
return self.convert_to_rdf().serialize(format='turtle').decode("utf-8")
299358

setup.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232

3333
install_req = ["lxml", "pyyaml>=5.1", "rdflib", "docopt", "pathlib"]
3434

35+
tests_req = ["owlrl", "requests"]
36+
3537
if sys.version_info < (3, 4):
3638
install_req += ["enum34"]
3739

@@ -45,6 +47,7 @@
4547
packages=packages,
4648
test_suite='test',
4749
install_requires=install_req,
50+
tests_require=tests_req,
4851
include_package_data=True,
4952
long_description=description_text,
5053
long_description_content_type="text/markdown",

0 commit comments

Comments
 (0)