OntoEnv from Python =================== This tutorial builds the same environment as :doc:`first-environment`, but from Python, and ends by running a SPARQL query against a resolved imports closure. About 10 minutes. You do not need to have done the CLI tutorial first. .. raw:: html

The minimum you need to know

Four calls cover almost every use of the Python API:

.. code-block:: python from ontoenv import OntoEnv env = OntoEnv.connect("./ontology-env") # 1. open (creating it if needed) name = env.add("./ontologies/site.ttl") # 2. register an ontology view, imported = env.get_closure(name) # 3. read it plus its imports env.close() # 4. release resources Everything below is that, slowed down. Install ------- .. code-block:: bash pip install ontoenv # Python 3.11+ Wheels are pre-built, so no Rust toolchain is required. You will also want ``rdflib``, which comes along as a dependency. Create some ontologies ---------------------- Make a working directory with two ontology files: .. code-block:: bash mkdir -p tutorial/ontologies cd tutorial ``ontologies/sensors.ttl``: .. code-block:: turtle @prefix owl: . @prefix rdfs: . @prefix sen: . a owl:Ontology . sen:Sensor a owl:Class ; rdfs:label "Sensor" . sen:Thermometer a owl:Class ; rdfs:subClassOf sen:Sensor . ``ontologies/site.ttl``: .. code-block:: turtle @prefix owl: . @prefix rdfs: . @prefix sen: . @prefix site: . a owl:Ontology ; owl:imports . site:Room a owl:Class ; rdfs:label "Room" . site:hasSensor a owl:ObjectProperty ; rdfs:domain site:Room ; rdfs:range sen:Sensor . ``site.ttl`` imports ``https://example.org/sensors`` by IRI. Nothing in the file says where that ontology lives; resolving that is OntoEnv's job. Connect to an environment ------------------------- .. code-block:: python from ontoenv import OntoEnv env = OntoEnv.connect( "./ontology-env", search_directories=["./ontologies"], ) env.update() print(env.get_ontology_names()) # ['https://example.org/sensors', 'https://example.org/site'] Two calls, two distinct jobs: ``connect`` opens the environment at ``./ontology-env``, creating it if it is not there. It is deliberately cheap — it does not read your ontology files. ``update`` is what scans ``search_directories`` for new and changed files, parses them, and follows their imports. Keeping these separate means restarting your program does not re-read every file on disk. Run the script a second time. ``connect`` reopens the saved environment directly, and ``update`` finds nothing new to do. Read a single graph ------------------- .. code-block:: python g = env.get_graph("https://example.org/site") print(len(g)) # just the triples in site.ttl ``get_graph`` returns a read-only ``rdflib.Graph`` backed by OntoEnv's storage. Nothing is copied, so this is fast even for large ontologies — but adding or removing triples raises ``ValueError``. When you want a graph you can edit, ask for a copy: .. code-block:: python from rdflib import Literal, URIRef g = env.copy_graph("https://example.org/site") g.add((URIRef("https://example.org/site#Room"), URIRef("http://www.w3.org/2000/01/rdf-schema#comment"), Literal("A room"))) This read-only-by-default, copy-on-request split runs through the whole API: every ``get_*`` method returns a view, and every ``copy_*`` method materializes a mutable ``rdflib`` object. :doc:`../explanation/views-and-copies` covers when each is the right choice. Resolve the imports closure --------------------------- .. code-block:: python view, imported = env.get_closure("https://example.org/site") print(imported) # ['https://example.org/site', 'https://example.org/sensors'] print(len(view)) # triples from both graphs, merged ``get_closure`` returns two things: a read-only view over the ontology plus all its transitive imports, and the list of graphs that went into it. The view is not a raw concatenation. Resolved ``owl:imports`` statements are stripped out, the imported graphs' ontology declarations are collapsed onto the root, and duplicate triples appear once. It is a single flattened graph that stands on its own. As with single graphs, ``copy_closure`` gives you the same content as a mutable ``rdflib.Graph``: .. code-block:: python g, imported = env.copy_closure("https://example.org/site") g.serialize("closure.ttl", format="turtle") Query it with SPARQL -------------------- The view supports ``query()`` directly, and the query executes in Rust against OntoEnv's storage rather than in rdflib's Python engine: .. code-block:: python view, _ = env.get_closure("https://example.org/site") rows = view.query(""" PREFIX rdfs: PREFIX owl: SELECT ?cls ?label WHERE { ?cls a owl:Class . OPTIONAL { ?cls rdfs:label ?label } } """) for row in rows: print(row.cls, row.label) The query sees classes from *both* files, because the closure merged them. That is the point: you write one query against one graph and the import resolution has already happened. Add an ontology from the web ---------------------------- .. code-block:: python name = env.add("https://brickschema.org/schema/1.4.4/Brick.ttl") print(name) # 'https://brickschema.org/schema/1.4/Brick' view, imported = env.get_closure(name) print(f"{len(imported)} graphs, {len(view)} triples") ``add`` fetches the URL, follows its ``owl:imports``, fetches those, and returns the ontology's canonical IRI — which, as here, is often not the same as the URL you downloaded it from. Remote copies are cached on disk. Close the environment --------------------- .. code-block:: python env.close() Or let a ``with`` block do it: .. code-block:: python with OntoEnv.connect("./ontology-env") as env: view, imported = env.get_closure("https://example.org/site") print(len(view)) The context manager is a convenience for scripts, not a requirement. A long-running server should connect once at startup and close at shutdown — see :doc:`../how-to/use-in-a-service`. Skip persistence entirely ------------------------- For a notebook or a test where nothing should be written to disk: .. code-block:: python env = OntoEnv(temporary=True) env.add("./ontologies/site.ttl") A temporary environment keeps everything in memory and leaves no files behind. It also has no saved index, so it starts from nothing every time. What you learned ---------------- - ``OntoEnv.connect(path)`` opens or creates a persistent environment; ``update()`` is the separate, explicit step that reads source files. - ``get_*`` returns fast read-only views; ``copy_*`` returns mutable ``rdflib`` objects. - ``get_closure`` merges an ontology with its transitive imports into one flattened graph, and that view answers SPARQL queries directly. - ``OntoEnv(temporary=True)`` gives you the same API with nothing persisted. Next steps ---------- - :doc:`../how-to/use-in-a-service` — connecting once and sharing the environment across requests. - :doc:`../how-to/query-with-sparql` — using the environment as an ``rdflib`` store and querying across named graphs. - :doc:`../reference/python` — every method, grouped by what it does.