From a8d838c8d268c054a3a369a3a98c08182958f19b Mon Sep 17 00:00:00 2001 From: Jan Pokorný Date: Fri, 22 Mar 2013 20:59:59 +0100 Subject: Now we can account for ontogen project, not a mere stylesheet anymore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also add README Signed-off-by: Jan Pokorný --- README | 26 ++++ ontogen.py | 390 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ ontogen.template | 105 +++++++++++++++ template.py | 116 +++++++++++++++++ 4 files changed, 637 insertions(+) create mode 100644 README create mode 100644 ontogen.py create mode 100644 ontogen.template create mode 100644 template.py 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ý +# 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 @@ + + + + + + + + + ${Ontology.version} + ${Ontology.priorVersion.base} + ${Ontology.label} + +${Ontology.comment} + + + ${Ontology.creator} + + ${Ontology.issued} + ${Ontology.modified} + + + + + + ${Class.label} + +${Class.comment} + + + ${Class.status} + + + + + + ${Property.label} + +${Property.comment} + + + + + ${Property.status} + + + + + + +${Example.comment} + + ${Example.pfx} + + + + + + + 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. + + + test + + """ + 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('''\ + # John Doe''') + 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 {})) -- cgit