A Python port of the Eyeling Notation3 reasoner.
RDF/Turtle/TriG compatibility through rdflib, RDF 1.2 surface-syntax checks, and RDF Message Log parsing/streaming.
The Eyeling repositories remains the main implementation.
- Python 3.10 or newer
piprdflibis installed automatically frompyproject.tomlpytestis needed only for the test suite
cd pyling
python -m venv .venv
. .venv/bin/activate # Windows: .venv\Scripts\activate
python -m pip install --upgrade pip
python -m pip install -e ".[test]"Verify the install:
pyling --help
python -m pytest -qRun a simple N3 rule program from standard input:
cat > example.n3 <<'EOF'
@prefix : <http://example.org/> .
:Socrates a :Man .
{ ?x a :Man } => { ?x a :Mortal } .
EOF
pyling example.n3Expected derived output:
@prefix : <http://example.org/> .
:Socrates a :Mortal .
Include explicit input facts in the rendered closure:
pyling --include-input-facts example.n3Read multiple sources, such as facts plus rules:
pyling facts.n3 rules.n3Enable RDF/Turtle/TriG/RDF Message compatibility mode:
pyling --rdf data.trig rules.n3Force a line format:
pyling --rdf --input-format nt data.nt
pyling --rdf --input-format nquads data.nqfrom pyling import reason, reason_stream, run_async
program = """
@prefix : <http://example.org/> .
:a :p :b .
{ ?s :p ?o } => { ?s :q ?o } .
"""
print(reason({}, program))
result = reason_stream(program, {"include_input_facts_in_closure": True})
print(result.closure_n3)
print(result.derived)Multi-source input mirrors Eyeling’s source-list style:
from pyling import reason
out = reason({}, {
"sources": [
"@prefix : <http://example.org/> .\n:Socrates a :Man .\n",
"@prefix : <http://example.org/> .\n{ ?x a :Man } => { ?x a :Mortal } .\n",
]
})The main exported term classes are Iri, Literal, Var, Blank, ListTerm, GraphTerm, Triple, Rule, and PrefixEnv.
RDF mode is selected with --rdf on the CLI or {"rdf": True} in the API. It routes ordinary RDF syntax through rdflib instead of the N3 rule parser:
from pyling import reason
rdf = """
PREFIX : <http://example.org/>
:a :p :b .
"""
print(reason({"rdf": True, "include_input_facts_in_closure": True}, rdf))Supported compatibility inputs include:
- Turtle /
.ttl - TriG /
.trig - N-Triples /
.nt - N-Quads /
.nq - uppercase
PREFIX,BASE, and RDF 1.2VERSIONsurface forms - simple RDF 1.2 annotation syntax, preserving the asserted triple
- RDF Message Logs using
VERSION "1.2-messages"andMESSAGE
The RDF 1.2 layer includes strict surface checks for common negative conformance cases such as surrogate numeric escapes, invalid RDF 1.2 language tags in line syntaxes, relative IRIREFs in N-Triples/N-Quads, and annotation syntax in line syntaxes.
Run the bundled offline RDF 1.2 smoke suite:
python tools/run_rdf12_w3c.pyRun the W3C RDF 1.2 syntax compliance manifests (requires Node.js and network access on the first run):
npm ci
npm run specThe W3C runner caches downloaded manifests and fixtures in
.rdf-test-suite-cache/. All RDF 1.2 syntax cases in the configured N-Triples,
N-Quads, Turtle, and TriG manifests are enabled.
Whole-log replay:
cat > messages.trig <<'EOF'
VERSION "1.2-messages"
PREFIX : <http://example.org/>
:a :value 21 .
MESSAGE
# Empty heartbeat message.
MESSAGE
:b :value 22 .
EOF
pyling --rdf --include-input-facts messages.trigStreaming replay, one message envelope at a time:
pyling --rdf --stream-messages rules.n3 messages.trigPython streaming API:
from pyling import reason_message_stream
for result in reason_message_stream({"sources": [rules_n3, messages_trig]}, {"rdf": True}):
print(result.closure_n3)The replay vocabulary uses:
@prefix eymsg: <https://eyereasoner.github.io/eyeling/vocab/message#> .
It materializes stream/envelope metadata, eymsg:orderedEnvelopes, eymsg:messageCount, eymsg:payloadKind, and payload formula links through log:nameOf. Rules can inspect each payload graph with log:includes.
The package includes the built-in registry API:
from pyling import register_builtin, unregister_builtin, list_builtin_irisBuilt-in coverage includes common predicates in these namespaces:
math:numeric comparison and arithmetic, plus common trig functionsstring:contains/matches/replace/format/length/comparison helperslist:first/rest/member/append/reverse/sort and related helperslog:includes/notIncludes/semantics/conjunction/skolem/uri and equality helpersdt:datatype inspection, validation, value comparison, canonicalizationcrypto:md5/sha/sha256/sha512time:year/month/day/hour/minute/second/timeZone/localTime
Registering a custom built-in:
from pyling import Literal, XSD_NS, register_builtin
def hello(ctx):
return [ctx.unify_term(ctx.goal.o, Literal("world", XSD_NS + "string"), ctx.subst)]
register_builtin("http://example.org/custom#hello", hello)The synchronous API uses in-memory reasoning. run_async(..., {"store": ...}) can persist explicit and inferred triples through the included JSON-backed persistent store.
import asyncio
from pyling import run_async
async def main():
result = await run_async(program, {"store": {"name": "demo", "path": ".eyeling-store", "clear": True}})
await result.store.close()
asyncio.run(main())Run the package tests:
python -m pytest -qRun the offline RDF 1.2 compatibility smoke suite:
python tools/run_rdf12_w3c.pyRun the W3C RDF 1.2 compliance tests:
npm ci
npm run specRun the external phochste/notation3tests suite when you have network access or an existing checkout:
# Existing checkout
python tools/run_notation3tests.py /path/to/notation3tests
# Or let the runner clone and install the public suite at https://codeberg.org/phochste/notation3tests
python tools/run_notation3tests.py --cloneThe runner defaults to NETWORKING=0, because this native port does not
dereference Web resources. It removes stale .out files for the 19 skipped
network fixtures so the reported score covers only tests actually executed.
You can also have pytest run the external suite by setting:
NOTATION3TESTS_DIR=/path/to/notation3tests python -m pytest -qrdflibis used only for RDF/Turtle/TriG/N-Triples/N-Quads parsing. N3 rules and formulas are parsed by the local parser.- RDF Message Logs are split and replayed before reasoning so message boundaries, empty heartbeat messages, and per-message blank-node scope are preserved.