diff options
-rw-r--r-- | README | 26 | ||||
-rw-r--r-- | ontogen.py | 390 | ||||
-rw-r--r-- | ontogen.template | 105 | ||||
-rw-r--r-- | template.py | 116 |
4 files changed, 637 insertions, 0 deletions
@@ -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 {})) |