Skip to main content

Pure-Python Manchester OWL Syntax parser and renderer for owlready2, with a class-relation SPARQL query builder. No Java required.

Project description

omny

PyPI Python License: MIT

Pure-Python Manchester OWL Syntax parser and renderer for owlready2, plus a store-agnostic SPARQL query builder for class-relation retrieval. No Java required.

pip install omny

Previously developed as pymos; renamed for PyPI release. See CHANGELOG.md for the migration note.


Quick taste

One small ontology — a class hierarchy and an anonymous class expression — exercises the three things omny is for end to end: round-trip (parse → render → re-parse with no loss), retrieve a class's axioms as RDF (including the blank-node body of any anonymous restrictions), and rerun the same query after a pure-Python reasoner has materialised the inferences — no Java involved.

import omny
from omny.store import run_rdflib
import owlrl  # `pip install omny[reasoning]` for owlrl  (used in step 3)

onto = omny.parse("""
Prefix: : <http://example.org/>

Class: Food
Class: Cheese
Class: Pizza        SubClassOf: Food
Class: Margherita
    SubClassOf: Pizza, hasTopping some Cheese

ObjectProperty: hasTopping

Individual: mozz    Types: Cheese
Individual: myPie
    Types: Margherita
    Facts: hasTopping mozz
""")

# 1) Round-trip: render → re-parse → re-render is byte-equal.  The class
#    hierarchy AND the anonymous restriction `hasTopping some Cheese`
#    survive cleanly.
text = omny.render(onto)
assert omny.render(omny.parse(text)) == text
print(f"round-trip OK ({len(text.splitlines())} lines, idempotent).")

# 2) Ask for Margherita's super-axioms as RDF, then render the SPARQL
#    result back to Manchester — the anonymous restriction
#    `hasTopping some Cheese` and the chain to `Food` both come back
#    intact, in human-readable syntax.
q = omny.class_relations_query("<http://example.org/Margherita>",
                                relations=("super",), construct=True)
result_graph = run_rdflib(q, onto.world.as_rdflib_graph())

import owlready2, io
result_world = owlready2.World()
result_onto = result_world.get_ontology("http://example.org/result/")
result_onto.load(
    fileobj=io.BytesIO(result_graph.serialize(format="nt").encode()),
    format="ntriples",
)
print(omny.render(result_onto, prefixes={"": "http://example.org/"}))
# Prefix: : <http://example.org/>
# Ontology: <http://example.org/result>
#
# ObjectProperty: :hasTopping
# Class: :Cheese
# Class: :Food
# Class: :Margherita
#     SubClassOf: :Pizza, :hasTopping some :Cheese
# Class: :Pizza
#     SubClassOf: :Food

# 3) Same shape of query, different question: which individuals belong to
#    Food?  The asserted graph says "none directly".  After a pure-Python
#    OWL 2 RL reasoner materialises subsumption (myPie a Margherita →
#    Pizza → Food), `myPie` appears.
q_ind = omny.class_relations_query("<http://example.org/Food>",
                                    relations=("individual",),
                                    construct=False)
asserted = onto.world.as_rdflib_graph()
print("Food individuals (asserted):",
      sorted(str(r[0]) for r in run_rdflib(q_ind, asserted)))
# Food individuals (asserted): []

import rdflib
reasoned = rdflib.Graph()
reasoned.parse(data=asserted.serialize(format="turtle"), format="turtle")
owlrl.DeductiveClosure(owlrl.OWLRL_Semantics).expand(reasoned)
print("Food individuals (reasoned):",
      sorted(str(r[0]) for r in run_rdflib(q_ind, reasoned)))
# Food individuals (reasoned): ['http://example.org/myPie']

The parsed value is a plain owlready2 Ontology, so the full owlready2 Python API (class hierarchy, axioms, instances, characteristics) applies — omny adds no separate object model to learn.

Why omny?

  • You have .omn files and want to work with them in Python without a JVM.
  • You want to ask "what are the subclasses / superclasses / equivalent classes / instances of X?" without writing the SPARQL by hand — and have the same query run against rdflib, pyoxigraph, owlready2's own engine, or a remote endpoint.
  • You're editing ontologies and need a lossless round-trip between Manchester text and the Python object model.
  • You'd like to do all of the above inside a Jupyter notebook, with %%mos cells and tab completion for axiom keywords and entity names.

For the full inventory — every supported frame, every axiom keyword, every SPARQL relation, every backend runner, every Jupyter magic — see docs/FEATURES.md.


Install

pip install -e .

Optional extras:

Extra What it adds
.[rdflib] rdflib ≥ 7.0 for run_rdflib
.[pyoxigraph] pyoxigraph ≥ 0.4 for run_pyoxigraph
.[endpoint] SPARQLWrapper ≥ 2.0 for run_endpoint
.[dev] all of the above + pytest + ruff

Core dependencies are parsimonious and owlready2 only.


Usage A — Parse a Manchester document

import omny

doc = """
Prefix: : <http://example.org/>

Class: Food

Class: Pizza
    SubClassOf: Food

Class: MargheritaPizza
    SubClassOf: Pizza
    EquivalentTo: Pizza and (hasTopping some MozzarellaTopping)
"""

onto = omny.parse(doc)

# Look up classes by full IRI
food       = onto.world["http://example.org/Food"]
pizza      = onto.world["http://example.org/Pizza"]
margherita = onto.world["http://example.org/MargheritaPizza"]

print(pizza.is_a)
# [owl.Thing, example.org.Food]

print(margherita.equivalent_to)
# [example.org.Pizza & example.org.hasTopping.some(example.org.MozzarellaTopping)]

parse returns an owlready2.Ontology. Pass an existing ontology as the onto argument to populate it in-place.


Usage B — Parse a single class expression

import owlready2
import omny

onto = owlready2.World().get_ontology("http://example.org/onto.owl")
with onto:
    class hasTopping(owlready2.ObjectProperty): pass
    class Cheese(owlready2.Thing): pass

expr = omny.parse_expression("hasTopping some Cheese", onto)
print(expr)        # onto.hasTopping.some(onto.Cheese)
print(type(expr))  # <class 'owlready2.class_construct.Restriction'>

parse_expression returns an owlready2 construct (a Restriction, And, Or, Not, OneOf, ConstrainedDatatype, or a named class) that can be appended directly to .is_a or .equivalent_to lists.


Usage C — Class-relation SPARQL retrieval

CONSTRUCT — retrieve the full RDF subgraph of related classes

import omny
from omny import class_relations_query
from omny.store import run_rdflib

doc = """
Prefix: : <http://example.org/>
Class: Food
Class: Pizza
    SubClassOf: Food
Class: MargheritaPizza
    SubClassOf: Pizza
"""
onto = omny.parse(doc)

# Build a CONSTRUCT query for the superclasses and subclasses of Pizza
q = class_relations_query(
    "<http://example.org/Pizza>",
    relations=("super", "sub"),
    construct=True,           # default
)

# Run against the owlready2 world via the rdflib adapter
result_graph = run_rdflib(q, onto.world.as_rdflib_graph())
# result_graph is an rdflib.Graph containing the subgraph of Food and
# MargheritaPizza (all their structural triples).
print({str(s) for s, p, o in result_graph})
# {'http://example.org/Food', 'http://example.org/Pizza',
#  'http://example.org/MargheritaPizza'}

SELECT — retrieve related IRIs only

from omny.store import run_owlready2

q_select = class_relations_query(
    "<http://example.org/Pizza>",
    relations=("super", "sub"),
    construct=False,
)

rows = run_owlready2(q_select, onto.world)
print([str(r[0]) for r in rows])
# ['owl.Thing', 'example.org.Food', 'example.org.MargheritaPizza']

Running against a pyoxigraph store

import io
import pyoxigraph
from omny.store import run_pyoxigraph

# Serialise the owlready2 world to N-Triples and load into pyoxigraph
nt_bytes = onto.world.as_rdflib_graph().serialize(format="nt").encode()

store = pyoxigraph.Store()
store.load(io.BytesIO(nt_bytes), format=pyoxigraph.RdfFormat.N_TRIPLES)

results = list(run_pyoxigraph(q_select, store))
print([str(s["rel"]) for s in results])
# ['<http://www.w3.org/2002/07/owl#Thing>',
#  '<http://example.org/Food>',
#  '<http://example.org/MargheritaPizza>']

Usage D — Render back to Manchester

omny.render(onto, prefixes=...) produces a Manchester OWL syntax document from an owlready2 ontology — the round-trip companion to parse.

import omny

doc = """
Prefix: : <http://example.org/>
Prefix: rdfs: <http://www.w3.org/2000/01/rdf-schema#>

Class: Pizza
    Annotations: rdfs:label "Pizza"
    SubClassOf: Food
    DisjointWith: IceCream

ObjectProperty: hasTopping
    Domain: Pizza
    Range: Topping
    Characteristics: Transitive

Individual: margherita1
    Types: Pizza
    Facts: hasTopping cheese1
"""

onto = omny.parse(doc)
text = omny.render(onto, prefixes={
    "": "http://example.org/",
    "rdfs": "http://www.w3.org/2000/01/rdf-schema#",
})
print(text)

render emits frames in stable order (Datatype → AnnotationProperty → ObjectProperty → DataProperty → Class → Individual), each sorted by IRI. Annotations, Facts:, SameAs:, DifferentFrom:, property Characteristics:, and InverseOf: are all rendered. A second pass is byte-identical — useful for deterministic diff-friendly output.

Render a single class expression

from omny import parse_expression, render_expression

prefixes = {"": "http://example.org/"}
ce = parse_expression("hasTopping some (Cheese or Tomato)", onto, prefixes=prefixes)
print(render_expression(ce, prefixes=prefixes))
# :hasTopping some (:Cheese or :Tomato)

render_expression is precedence-aware: lower-precedence operands (or) are parenthesised inside higher-precedence parents (and) automatically.

Round-trip

text1 = omny.render(omny.parse(doc), prefixes=prefixes)
text2 = omny.render(omny.parse(text1), prefixes=prefixes)
assert text1 == text2   # idempotent

parse → render → parse preserves the set of class / property / individual IRIs and the count of axioms per entity.


Usage E — Navigating the owlready2 model

omny.parse() returns a real owlready2.Ontology, so the full owlready2 Python OWL API applies — omny adds no separate object API of its own. Once an ontology is parsed you can walk the class hierarchy, inspect axioms, list instances, and read property characteristics directly.

import omny

doc = """
Prefix: : <http://example.org/>

Class: Food
Class: Pizza
    SubClassOf: Food
Class: Margherita
    SubClassOf: Pizza
Class: Capricciosa
    SubClassOf: Pizza

ObjectProperty: hasTopping
    Domain: Pizza
    Range: Food

Individual: m1
    Types: Margherita
"""
onto = omny.parse(doc)
Pizza = onto.world["http://example.org/Pizza"]
hasT  = onto.world["http://example.org/hasTopping"]

# --- class navigation ---
Pizza.is_a               # [owl.Thing, example.org.Food] (direct supers + restrictions)
list(Pizza.subclasses())  # [Margherita, Capricciosa] (direct only)
list(Pizza.descendants()) # [Pizza, Margherita, Capricciosa] (incl. self, transitive)
list(Pizza.ancestors())   # [Pizza, owl.Thing, Food] (incl. self, transitive)
Pizza.equivalent_to       # equivalent classes (writable list)
list(Pizza.instances())   # [m1] — direct instances (no reasoning)

# --- property navigation ---
list(hasT.domain)   # [Pizza]
list(hasT.range)    # [Food]
hasT.is_a           # superproperties + characteristic mixins

# --- individuals carry their own attribute accessors ---
m1 = onto.world["http://example.org/m1"]
m1.is_a               # [owl.Thing, Margherita]
type(m1).__name__     # 'Margherita'   (owlready2 maps individuals to their Python class)

No reasoning runs by default. .descendants() / .ancestors() / .instances() walk only asserted axioms. To pick up inferred relations, materialise them first — see Reasoning below and notebook examples/notebooks/06_reasoning.ipynb.


Reasoning

omny itself is reasoner-free, but the owlready2 ontology it returns can be fed to any reasoner that integrates with owlready2 or with an RDF graph:

Reasoner Profile Wrapper Java?
owlrl OWL 2 RL pure-Python (rdflib) no
HermiT / Pellet OWL 2 DL owlready2 + JPype bridge yes
HermiT / JFact / ELK DL / EL ROBOT docker (robot reason) yes (docker)
Konclude OWL 2 DL konclude docker no JVM (C++)

The simplest pattern uses owlrl in-process — pure Python, no Java:

import io, omny, owlrl, rdflib

onto = omny.parse(open("ontology.omn").read())

# owlready2 → rdflib graph → expand under OWL 2 RL semantics
buf = io.BytesIO(); onto.save(file=buf, format="ntriples")
g = rdflib.Graph(); g.parse(data=buf.getvalue(), format="nt")
owlrl.DeductiveClosure(owlrl.OWLRL_Semantics).expand(g)

# Query the saturated graph with the same omny.class_relations_query
from omny import class_relations_query
from omny.store import run_rdflib
q = class_relations_query("<http://example.org/Pizza>", relations=("sub",))
inferred = run_rdflib(q, g)

For DL reasoning use owlready2's sync_reasoner_hermit() (requires a JDK):

import owlready2

with onto:
    owlready2.sync_reasoner_hermit(infer_property_values=True)

# Now Pizza.descendants() / .equivalent_to / .is_a reflect HermiT inferences.

See examples/notebooks/06_reasoning.ipynb for a runnable walk-through that compares the asserted graph against owlrl + HermiT materialisations on the same ontology.


Relation table

Relation Semantics
super Transitive superclasses — all classes reachable via rdfs:subClassOf+ upward from the target.
sub Transitive subclasses — all classes reachable via rdfs:subClassOf+ downward from the target.
direct_super Immediate superclasses — one rdfs:subClassOf step up, with intermediate classes filtered out.
direct_sub Immediate subclasses — one rdfs:subClassOf step down, with intermediate classes filtered out.
equiv Equivalent classes — both directions of owl:equivalentClass.
individual Instances of the target class — subjects of rdf:type triples.

Anonymous expression targets

class_relations_query accepts an anonymous Manchester class expression as its target. Parse the expression first with parse_expression, then pass the returned owlready2 construct directly:

import omny
from omny import class_relations_query
from omny.store import run_rdflib

onto = omny.parse(open("pizza.omn").read())
expr = omny.parse_expression(
    "hasTopping only (Cheese or Tomato)",
    onto,
    prefixes={"": "http://ex.org/"},   # see note below
)

q = class_relations_query(expr, relations=("equiv",), construct=False)
rows = [str(r[0]) for r in run_rdflib(q, onto.world.as_rdflib_graph())]
print(rows)
# ['http://ex.org/Margherita']

The generated SPARQL contains a structural sub-pattern that matches the blank-node shape owlready2 writes for the expression, binding a fresh variable (?t0) to any matching node. The relation clauses then use that variable.

Supported constructs: R some C, R only C, R value v, R Self, R min/max/exactly N [C] (qualified + unqualified), A and B, A or B, not A, {a, b, ...}, inverse R, and arbitrary nesting.

Limitations

  • Operand order matters. Two structurally equivalent expressions with permuted intersection/union operands do not match each other. (A and B and B and A produce different patterns; only the as-declared order matches the blank-node spine.)
  • Structural identity only. With no reasoning, semantically equivalent but structurally distinct expressions do not match (e.g. an EquivalentTo axiom defined via an intermediate named class is invisible to the structural pattern).
  • Data ranges (ConstrainedDatatype) and literal hasValue targets are not supported. Use a named individual (hasTopping value myCheese) rather than a literal (age value 42).

Namespace note

parse_expression resolves bare names (e.g. Cheese, hasTopping) against onto.base_iri, NOT against the document's Prefix: : declaration. If your ontology declares a Prefix: : that differs from its Ontology: <...> IRI — which is common — pass the empty-prefix mapping explicitly:

expr = omny.parse_expression(
    "hasTopping only (Cheese or Tomato)",
    onto,
    prefixes={"": "http://ex.org/"},
)

Without this override, bare names resolve to fresh entities under onto.base_iri that don't exist in the loaded graph, and the query returns no rows.


Caveats

  • No Java required. omny is pure Python; it does not call a DL reasoner or require an OWL API JVM.
  • Asserted graph only, no reasoning. omny loads and queries only the explicitly stated axioms. Inferred subclass / equivalence relations are not visible unless a reasoner has already materialised them into the graph.
  • CONSTRUCT returns full outgoing subgraphs. A CONSTRUCT query retrieves not just the related class IRI but the entire structural outgoing subgraph of that class (i.e. all its restriction blank-nodes, list nodes, etc.). This is intentional — it allows a client to reconstruct anonymous class expressions without further round-trips. Use construct=False if you only need the IRIs.
  • run_owlready2 is SELECT-only. owlready2's built-in SPARQL engine cannot parse CONSTRUCT queries. For CONSTRUCT against owlready2 data use run_rdflib(q, world.as_rdflib_graph()).
  • Frame tokeniser is not string-aware. A token that looks like Keyword: at the start of a line inside a multi-line quoted literal can cause incorrect frame splitting. Single-line operands and standard Manchester frame forms work correctly. Import: directives in the ontology preamble are recorded as owl:imports declarations (visible via onto.imported_ontologies) but the imported ontologies are not fetched — only the declaration is stored.

Attribution

The Manchester OWL Syntax PEG grammar is vendored from owlapy (MIT licence, © 2024 Caglar Demir). See NOTICE and licenses/owlapy-LICENSE.txt.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

omny-0.1.1.tar.gz (71.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

omny-0.1.1-py3-none-any.whl (51.3 kB view details)

Uploaded Python 3

File details

Details for the file omny-0.1.1.tar.gz.

File metadata

  • Download URL: omny-0.1.1.tar.gz
  • Upload date:
  • Size: 71.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for omny-0.1.1.tar.gz
Algorithm Hash digest
SHA256 6772a466f84ba90187233d8ed27402dcd7238b74b8f80ebaee114442cdb579a3
MD5 9a13c0eaf5dbaa47376a72acae30f1bb
BLAKE2b-256 6763579231b4c207d4b1cc7086f8f31c2acdeebfe5ddc90830c980590f7c7765

See more details on using hashes here.

Provenance

The following attestation bundles were made for omny-0.1.1.tar.gz:

Publisher: publish.yml on MaastrichtU-IDS/omny

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file omny-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: omny-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 51.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for omny-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 d3004ee1052341ae07bab52d004a5bafe308d6f0c796b4194873886da75d1c26
MD5 a92d69dc7372764e335b247e15ff12d8
BLAKE2b-256 ad70e221834b5dcf7bc58602815bf68a9f759888a7ea370a5a7d2326c9576122

See more details on using hashes here.

Provenance

The following attestation bundles were made for omny-0.1.1-py3-none-any.whl:

Publisher: publish.yml on MaastrichtU-IDS/omny

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page