summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README26
-rw-r--r--ontogen.py390
-rw-r--r--ontogen.template105
-rw-r--r--template.py116
4 files changed, 637 insertions, 0 deletions
diff --git a/README b/README
new file mode 100644
index 0000000..8287119
--- /dev/null
+++ b/README
@@ -0,0 +1,26 @@
+Ontogen
+-------
+
+The goal of this microproject is to ease maintenance of RDF schemas,
+vocabularies, ontologies... you name it.
+
+Especially important for the author was to have some convenient
+repository of representing the model to be externalized as RDF/XML,
+and this model is a simple form of Python code that is, up to
+some exceptions, imperative-less in nature.
+
+Also documentation is an important aspect covered by Ontogen,
+or more specifically, by ns-schema XSL template authored by
+Masahide Kanzaki. Luckily he published it under LGPLv2.1 so
+there was no need to reinvent the wheel and the implication
+is that this whole project is under LGPLv2.1 as well (see LICENSE).
+
+
+Primary development is centered around author's needs but any
+collaboration is welcome.
+
+As a kind of quick intro to starting custom vocabularies,
+template.py file is provided.
+
+
+Jan Pokorny, maintainer
diff --git a/ontogen.py b/ontogen.py
new file mode 100644
index 0000000..75318a0
--- /dev/null
+++ b/ontogen.py
@@ -0,0 +1,390 @@
+# vim: set fileencoding=UTF-8:
+# Copyright 2013 Red Hat, Inc.
+# Author: Jan Pokorný <jpokorny at redhat dot com>
+# Licensed under LGPL v2.1 (the same as original ns-schema.xsl)
+
+from sys import stdout, stderr
+from datetime import date
+from textwrap import TextWrapper, dedent
+from os import extsep, mkdir, symlink, remove
+from os.path import basename, exists, isdir, islink, splitext
+from subprocess import call
+from shlex import split
+#import codecs
+
+import logging
+
+logging.basicConfig()
+log = logging.getLogger(__name__)
+
+
+VERSIONSEP = '/'
+ONTSEP = '#'
+
+TEMPLATE_GENSHI = 'ontogen.template'
+TEMPLATE_XSLT = 'ns-schema.xsl'
+TEXT_INDENT = ' '
+TW = TextWrapper(initial_indent=TEXT_INDENT, subsequent_indent=TEXT_INDENT)
+tw = lambda x: TW.fill(x)
+twd = lambda x: tw(dedent(x))
+norm = lambda x: reduce(
+ lambda a, b: a and a + (b.islower() and b or '-' + b.lower()) or b.lower(),
+ x,
+ ''
+).replace('_', '-')
+
+
+#
+# Helpers
+#
+
+
+# classics: http://stackoverflow.com/a/1383402
+class ClassProperty(property):
+ def __get__(self, this, owner):
+ return self.fget.__get__(None, owner)()
+
+
+# another common fixer (adjusted): http://stackoverflow.com/a/13937525
+class InheritDocstring(type):
+ def __init__(this, name, bases, attrs):
+ if getattr(this, '__doc__', None) is None:
+ setattr(this, '__doc__', super(this, this).__doc__)
+
+
+def force_symlink(source, link_name):
+ if exists(link_name):
+ if islink(link_name):
+ remove(link_name)
+ else:
+ raise RuntimeError('{0} is in our way'.format(link_name))
+ symlink(source, link_name)
+
+
+#
+# Pure documentation level
+#
+
+
+class Example(object):
+ """Important data regarding examples"""
+ @ClassProperty
+ @classmethod
+ def comment(this):
+ return tw('\n'.join(this.__doc__.strip().splitlines()[:1]))
+
+ @ClassProperty
+ @classmethod
+ def pfx(this):
+ return ''
+
+ @ClassProperty
+ @classmethod
+ def code(this):
+ return '\n'.join(map(str.rstrip, this.__doc__.strip().splitlines()[2:]))
+
+ @ClassProperty
+ @classmethod
+ def image(this):
+ return ''
+
+
+#
+# Abstract RDF level
+#
+
+
+class RDFEntity(object):
+ """Common base for property, class and ontology"""
+
+ __metaclass__ = InheritDocstring
+
+ @ClassProperty
+ @classmethod
+ def id(this):
+ return ONTSEP + norm(this.__name__)
+
+ @ClassProperty
+ @classmethod
+ def namespaces(this):
+ return {}
+
+ @ClassProperty
+ @classmethod
+ def label(this):
+ return '\n'.join(this.__doc__.strip().splitlines()[:1]).strip()
+
+ @ClassProperty
+ @classmethod
+ def comment(this):
+ return '\n\n'.join(
+ map(
+ twd,
+ '\n'.join(map(
+ str.rstrip,
+ this.__doc__.strip().splitlines()[2:])).split('\n\n')
+ )
+ )
+
+ @ClassProperty
+ @classmethod
+ def status(this):
+ return ''
+
+
+#
+# RDF entities
+#
+
+
+class Class(RDFEntity):
+ """Important data regarding RDF class"""
+
+ # decorators
+
+ @classmethod
+ def inDomainOf(this, prop):
+ """Tells that decorated Property has decorating Class as a domain"""
+ assert issubclass(prop, Property)
+ if not prop.domain:
+ prop.domain = this.id
+ else:
+ log.warning('{0} already has domain set'
+ .format(prop))
+ return prop
+
+ @classmethod
+ def inRangeOf(this, prop):
+ """Tells that decorated Property has decorating Class as a range"""
+ assert issubclass(prop, Property)
+ if not prop.range:
+ prop.range = this.id
+ else:
+ log.warning('{0} already has range set'
+ .format(prop))
+ return prop
+
+ # members
+
+ @ClassProperty
+ @classmethod
+ def subClassOf(this):
+ return '' if this.__base__ == Class else this.__base__.id
+
+
+class Property(RDFEntity):
+ """Important data regarding RDF property"""
+
+ # members
+
+ @ClassProperty
+ @classmethod
+ def domain(this):
+ return ''
+
+ @ClassProperty
+ @classmethod
+ def range(this):
+ return ''
+
+ @ClassProperty
+ @classmethod
+ def subPropertyOf(this):
+ return '' if this.__base__ == Property else this.__base__.id
+
+
+class Ontology(RDFEntity):
+ """Important data regarding ontology"""
+
+ # decorators
+
+ @classmethod
+ def supersededBy(this, onto):
+ """Tells that decorated Ontology supersedes the decorating Ontology"""
+ assert issubclass(onto, Ontology)
+ if onto.priorVersion is None:
+ onto.priorVersion = this
+ else:
+ log.warning('{0} already has priorVersion set'.format(onto))
+ return onto
+
+ # members
+
+ @ClassProperty
+ @classmethod
+ def base_uri(this):
+ raise NotImplementedError
+
+ @ClassProperty
+ @classmethod
+ def version(this):
+ # sorting key
+ raise NotImplementedError
+
+ @ClassProperty
+ @classmethod
+ def base(this):
+ return this.base_uri + VERSIONSEP + this.version
+
+ @ClassProperty
+ @classmethod
+ def creator(this):
+ return ''
+
+ @ClassProperty
+ @classmethod
+ def issued(this):
+ return ''
+
+ @ClassProperty
+ @classmethod
+ def modified(this):
+ return date.today().isoformat()
+
+ @ClassProperty
+ @classmethod
+ def priorVersion(this):
+ return None
+
+ @ClassProperty
+ @classmethod
+ def classes(this):
+ return []
+
+ @ClassProperty
+ @classmethod
+ def properties(this):
+ return []
+
+ @ClassProperty
+ @classmethod
+ def examples(this):
+ return []
+
+ # extras
+
+ @classmethod
+ def valid(this):
+ proper_subclasses = all((
+ all(issubclass(c, Class) for c in this.classes),
+ all(issubclass(p, Property) for p in this.properties),
+ all(issubclass(e, Example) for e in this.examples),
+ ))
+ if not proper_subclasses:
+ log.warning('Not proper subclasses used')
+
+ ids = [
+ x.id for x in
+ reduce(lambda a, b: a + b,
+ (getattr(this, ent_name) for ent_name in ('classes',
+ 'properties')))
+ ]
+ unique_ids = len(ids) == len(set(ids))
+ if not unique_ids:
+ log.warning('Not unique IDs used')
+
+ return proper_subclasses and unique_ids
+
+ @classmethod
+ def _auto_filename(this):
+ base = this.base_uri.rpartition('/')[2]
+ if not exists(base):
+ mkdir(base)
+ elif not isdir(base):
+ raise RuntimeError('{0} is not a directory'.format(base))
+ return (base + VERSIONSEP + this.version + extsep + 'rdf',
+ base + extsep + 'rdf')
+
+ @classmethod
+ def gendoc(this, infile, outfile, template=TEMPLATE_XSLT):
+ cmd = 'xsltproc --stringparam xmlfile {reference} -o {outfile}' \
+ ' {template} {infile}'.format(
+ reference=basename(infile),
+ infile=infile,
+ outfile=outfile,
+ template=template
+ )
+ log.debug('running {0}'.format(split(cmd)))
+ if call(split(cmd)):
+ raise RuntimeError('Something went wrong while running {0}'
+ .format(cmd))
+
+ @classmethod
+ def generate(this, ontologies=(), template=None, outfile='-', gendoc=True):
+ from genshi.template import MarkupTemplate
+
+ namespaces = {}
+ for ns, ns_uri in reduce(
+ lambda a, b: a + b.namespaces.items(),
+ [this] + this.classes + this.properties,
+ []
+ ):
+ if namespaces.setdefault('xmlns:' + ns, ns_uri) != ns_uri:
+ print >>stderr, 'NS clash: {0} vs {1}'.format(
+ namespaces['xmlns:' + ns], ns_uri)
+
+ tmpl_filename = template or TEMPLATE_GENSHI
+ tmpl_fileobj = open(tmpl_filename)
+ #tmpl_fileobj = codecs.open(tmpl_filename, encoding='UTF-8')
+ tmpl = MarkupTemplate(tmpl_fileobj, tmpl_filename)
+ tmpl_fileobj.close()
+
+ symfile = None
+ if outfile is '-':
+ outobj = stdout
+ else:
+ if outfile == 'AUTO':
+ outfile, symfile = this._auto_filename()
+ outobj = open(outfile, "w")
+
+ tmpldict = dict(Ontology=this, namespaces=namespaces)
+ print >>outobj, \
+ tmpl.generate(**tmpldict).render('xml', strip_whitespace=False)
+ outobj.flush() # usually failing without this
+
+ # htmldoc
+ htmlfile = None
+ if gendoc and outfile != '-':
+ htmlfile = splitext(outfile)[0] + extsep + 'html'
+ this.gendoc(outfile, htmlfile)
+
+ # symlink to base (non-versioned) only for the newest ontology
+ if ontologies[-1] is this and symfile:
+ force_symlink(outfile, symfile)
+ if htmlfile:
+ force_symlink(htmlfile, splitext(symfile)[0] + extsep + 'html')
+
+ if outobj is not stdout:
+ outobj.close()
+
+ @classmethod
+ def __str__(this):
+ this.generate()
+
+
+#
+# Generalized view
+#
+
+class Ontologies(object):
+ """Groups multiple versions of ontologies (presumably of the same base)"""
+ def __init__(self, ontologies=()):
+ self._ontologies = []
+ if ontologies:
+ self.include(*ontologies)
+
+ # decorators
+
+ def include(self, *ontologies):
+ assert all(map(lambda o: o.valid(), ontologies))
+ assert reduce(lambda a, b: issubclass(a, b) or issubclass(b, a),
+ ontologies)
+ merge = self._ontologies + list(ontologies)
+ self._ontologies = list(sorted(set(merge), key=lambda o: o.version))
+ # make it decorator-compatible
+ return (lambda first=None, *others: first)(*ontologies)
+
+ # members
+
+ def generate_latest(self, **kwargs):
+ self._ontologies[-1].generate(tuple(self._ontologies), **kwargs)
diff --git a/ontogen.template b/ontogen.template
new file mode 100644
index 0000000..7bdc287
--- /dev/null
+++ b/ontogen.template
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--!
+ Copyright 2013 Red Hat, Inc.
+ Author: Jan Pokorný <jpokorny at redhat dot com>
+ Licensed under LGPL v2.1 (the same as original ns-schema.xsl)
+ -->
+<rdf:RDF
+ xml:base="${Ontology.base}"
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:owl="http://www.w3.org/2002/07/owl#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#"
+ xmlns:py="http://genshi.edgewall.org/"
+ xmlns:vs="http://www.w3.org/2003/06/sw-vocab-status/ns#"
+ py:attrs="namespaces">
+
+ <!--
+ ONTOLOGY
+ -->
+
+ <owl:Ontology rdf:about="">
+ <owl:imports rdf:resource="http://purl.org/dc/elements/1.1/"/>
+ <owl:versionInfo>${Ontology.version}</owl:versionInfo><!--!
+ --><py:if test="Ontology.priorVersion">
+ <owl:priorVersion>${Ontology.priorVersion.base}</owl:priorVersion><!--!
+ --></py:if><py:if test="Ontology.label">
+ <rdfs:label>${Ontology.label}</rdfs:label><!--!
+ --></py:if><py:if test="Ontology.comment">
+ <rdfs:comment>
+${Ontology.comment}
+ </rdfs:comment><!--!
+ --></py:if><py:if test="Ontology.creator">
+ <dc:creator>
+ ${Ontology.creator}
+ </dc:creator><!--!
+ --></py:if>
+ <dc:issued>${Ontology.issued}</dc:issued><py:if test="Ontology.modified">
+ <dc:modified>${Ontology.modified}</dc:modified></py:if>
+ </owl:Ontology>
+
+ <!--
+ CLASSES
+ -->
+<!--! --><py:for each="Class in Ontology.classes">
+ <rdf:Class rdf:about="${Class.id}"><!--!
+ --><py:if test="Class.label">
+ <rdfs:label>${Class.label}</rdfs:label><!--!
+ --></py:if><py:if test="Class.comment">
+ <rdfs:comment>
+${Class.comment}
+ </rdfs:comment><!--!
+ --></py:if><py:if test="Class.subClassOf">
+ <rdfs:subClassOf rdf:resource="${Class.subClassOf}"/><!--!
+ --></py:if><py:if test="Class.status">
+ <vs:term-status>${Class.status}</vs:term-status><!--!
+ --></py:if>
+ </rdf:Class>
+<!--!
+ --></py:for>
+ <!--
+ PROPERTIES
+ -->
+<!--! --><py:for each="Property in Ontology.properties">
+ <rdf:Property rdf:about="${Property.id}"><!--!
+ --><py:if test="Property.label">
+ <rdfs:label>${Property.label}</rdfs:label><!--!
+ --></py:if><py:if test="Property.comment">
+ <rdfs:comment>
+${Property.comment}
+ </rdfs:comment><!--!
+ --></py:if><py:if test="Property.subPropertyOf">
+ <rdfs:subPropertyOf rdf:resource="${Property.subPropertyOf}"/><!--!
+ --></py:if><py:if test="Property.domain">
+ <rdfs:domain rdf:resource="${Property.domain}"/><!--!
+ --></py:if><py:if test="Property.range">
+ <rdfs:range rdf:resource="${Property.range}"/><!--!
+ --></py:if><py:if test="Property.status">
+ <vs:term-status>${Property.status}</vs:term-status><!--!
+ --></py:if>
+ </rdf:Property>
+<!--!
+ --></py:for>
+ <!--
+ EXAMPLES
+ -->
+<!--! --><py:for each="Example in Ontology.examples">
+ <ex:Example xmlns:ex="http://purl.org/net/ns/ex#"><!--!
+ --><py:if test="Example.comment">
+ <rdfs:comment>
+${Example.comment}
+ </rdfs:comment><!--!
+ --></py:if><py:if test="Example.pfx">
+ <ex:pfx>${Example.pfx}</ex:pfx><!--!
+ --></py:if><py:if test="Example.code">
+ <ex:code><![CDATA[
+${Example.code}
+]]></ex:code><!--!
+ --></py:if><py:if test="Example.image">
+ <ex:image rdf:resource="${Example.image}"/><!--!
+ --></py:if>
+ <ex:exampleOf rdf:resource=""/>
+ </ex:Example>
+ </py:for>
+<!-- vim: set et noai sts=2 sw=2 ft=xml: -->
+</rdf:RDF>
diff --git a/template.py b/template.py
new file mode 100644
index 0000000..e2a1a39
--- /dev/null
+++ b/template.py
@@ -0,0 +1,116 @@
+#!/usr/bin/env python
+# Example quickstart template for designing new ontologies
+
+from sys import argv
+#from genshi.input import XML
+
+from ontologygen import Property, Class, Example, Ontology, Ontologies
+
+BASE = 'http://purl.org/net/foo'
+
+
+#
+# classes
+#
+
+
+# v0.1
+
+class Foo(Class):
+ """abstract foo item, use only its subclasses"""
+
+
+class TFoo(Foo):
+ """T-shaped foo
+
+ Represents foos that are special because of the T-shape.
+ """
+
+#
+# properties
+#
+
+# v0.1
+
+@Foo.inDomainOf
+class bar(Property):
+ """plain bar
+
+ Connects resources with foos.
+ """
+ range = "http://www.w3.org/2001/XMLSchema#string"
+
+
+class baz(bar):
+ """bar specialization known as baz"""
+
+
+#
+# examples
+#
+
+# v0.1
+
+class ex_0_1_schema(Example):
+ """Illustrative figure of the vocabulary."""
+ image = '0.2.svg'
+
+
+class ex_0_1(Example):
+ """An example snippet.
+
+<foo:tfoo about="#mytfoo">
+ <foo:baz>test</foo:baz>
+</foo:tfoo>
+ """
+ pfx = 'foo:'
+
+
+#
+# ontology + versions
+#
+
+
+ontologies = Ontologies()
+
+
+class foo(Ontology):
+ """Descriptive vocabulary for foos
+
+ This vocabulary serves a purpose of shedding light into semantics
+ in the field of foos.
+ """
+ base_uri = BASE
+ #creator = XML('''\
+ # <dc:foo xmlns:dc="http://purl.org/dc/elements/1.1/"
+ # >John Doe</dc:foo>''')
+ creator = 'John Doe'
+
+
+@ontologies.include
+class foo_0_1(foo):
+ version = '0.1'
+ issued = '2013-02-19'
+ modified = '2013-03-20'
+ classes = [
+ Foo,
+ TFoo,
+ ]
+ properties = [
+ bar,
+ baz,
+ ]
+ examples = [
+ ex_0_1_schema,
+ ex_0_1,
+ ]
+
+
+#@ontologies.include
+#@foo_0_1.supersededBy
+#class foo_0_2(foo):
+# ...
+
+
+if __name__ == '__main__':
+ ontologies.generate_latest(**(len(argv) > 1 and {'outfile': argv[1]} or {}))