Skip to content

Commit

Permalink
Adding KGCL support via kgcl-rdflib (#164)
Browse files Browse the repository at this point in the history
* Adding bindings for kgcl-rdflib

* recognizing ofn suffix and rdflib prefix

* Adding some preliminary KGCL bindings.
These may move to another package - see althonos/pronto#180

* fixing signature

* adding skos definition

* Adding an apply command for applying KGCL patches
Adding a KGCL diff
Rationalizing other commands

* kgcl

* diffs

* lint

* poetry update

* plac8 flake8

* plac8 flake8

* plac8 flake8
  • Loading branch information
cmungall authored Jul 7, 2022
1 parent fd7b106 commit 3420110
Show file tree
Hide file tree
Showing 13 changed files with 282 additions and 305 deletions.
320 changes: 51 additions & 269 deletions poetry.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ ratelimit = "^2.2.1"
appdirs = "^1.4.4"
semsql = "^0.1.6"
lark = "^1.1.2"
kgcl = "^0.1.0"
kgcl-schema = "^0.1.1"
kgcl-schema = "^0.2.0"
funowl = "^0.1.12"
kgcl-rdflib = "^0.2.0"
pystow = "^0.4.4"

[tool.poetry.dev-dependencies]
Expand Down
136 changes: 122 additions & 14 deletions src/oaklib/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
)

import click
import kgcl_schema.grammar.parser as kgcl_parser
import rdflib
import sssom.writers as sssom_writers
import yaml
Expand Down Expand Up @@ -1497,6 +1498,12 @@ def labels(terms, output: TextIO, display: str, output_type: str):
runoak -i cl.owl labels CL:4023094
You can use the ".all" selector to show all labels:
Example:
runoak -i cl.owl labels .all
"""
impl = settings.impl
writer = _get_writer(output_type, impl, StreamingCsvWriter)
Expand All @@ -1508,6 +1515,36 @@ def labels(terms, output: TextIO, display: str, output_type: str):
writer.emit(dict(id=curie, label=label))


@main.command()
@click.argument("terms", nargs=-1)
@output_option
@display_option
@ontological_output_type_option
def definitions(terms, output: TextIO, display: str, output_type: str):
"""
Show definitions for terms
Example:
runoak -i sqlite:obo:envo definitions 'tropical biome' 'temperate biome'
You can use the ".all" selector to show all definitions:
Example:
runoak -i sqlite:obo:envo definitions .all
"""
impl = settings.impl
writer = _get_writer(output_type, impl, StreamingCsvWriter)
writer.display_options = display.split(",")
writer.file = output
for curie in query_terms_iterator(terms, impl):
if isinstance(impl, BasicOntologyInterface):
defn = impl.get_definition_by_curie(curie)
writer.emit(dict(id=curie, definition=defn))


@main.command()
@click.argument("terms", nargs=-1)
@predicates_option
Expand Down Expand Up @@ -1658,7 +1695,11 @@ def mappings(terms, maps_to_source, output, output_type):
@click.argument("terms", nargs=-1)
def aliases(terms, output, obo_model):
"""
List all aliases in the ontology
List aliases for a term or set of terms
Example:
runoak -i ubergraph:uberon aliases UBERON:0001988
Example:
Expand Down Expand Up @@ -1728,11 +1769,11 @@ def subset_rollups(subsets: list, output):
@main.command()
@output_option
@output_type_option
def axioms(output: str, output_type: str):
def all_axioms(output: str, output_type: str):
"""
List all axioms
EXPERIMENTAL
TODO: this will be replaced by "axioms" command
"""
impl = settings.impl
if isinstance(impl, OwlInterface):
Expand All @@ -1751,14 +1792,19 @@ def axioms(output: str, output_type: str):
@click.option("--axiom-type", help="Type of axiom, e.g. SubClassOf")
@click.option("--about", help="CURIE that the axiom is about")
@click.option("--references", multiple=True, help="CURIEs that the axiom references")
def filter_axioms(output: str, output_type: str, axiom_type: str, about: str, references: tuple):
@click.argument("terms", nargs=-1)
def axioms(terms, output: str, output_type: str, axiom_type: str, about: str, references: tuple):
"""
Filters axioms
"""
impl = settings.impl
if terms:
curies = [curie for curie in query_terms_iterator(terms, impl)]
else:
curies = None
if isinstance(impl, OwlInterface):
conditions = AxiomFilter(about=about)
conditions = AxiomFilter(about=curies)
if references:
conditions.references = list(references)
if axiom_type:
Expand Down Expand Up @@ -2166,20 +2212,82 @@ def diff_terms(output, other_ontology, terms):


@main.command()
@click.option("--other-ontology", help="other ontology")
@click.option("-X", "--other-ontology", help="other ontology")
@click.option(
"--simple/--no-simple",
default=False,
show_default=True,
help="perform a quick difference showing only terms that differ",
)
@output_option
@output_type_option
def diff_ontologies(output, output_type, other_ontology):
def diff(simple: bool, output, output_type, other_ontology):
"""
EXPERIMENTAL
Diff between two ontologies
The --simple option will compare the lists of terms in each ontology. This is currently
implemented for most endpoints.
If --simple is not set, then this will do a complete diff, and return the diff as KGCL
change commands.
Current limitations
- complete diffs can only be done using local RDF files
- Parsing using rdflib can be slow
- Currently the return format is ONLY the KGCL change DSL. In future YAML, JSON, RDF will be an option
"""
# TODO: include KGCL datamodel
impl = settings.impl
writer = _get_writer(output_type, impl, StreamingJsonLinesWriter)
writer.output = output
other_impl = get_implementation_from_shorthand(other_ontology)
if isinstance(impl, DifferInterface):
for diff in impl.compare_ontology_term_lists(other_impl):
writer.emit(diff)
if simple:
for change in impl.compare_ontology_term_lists(other_impl):
writer.emit(change)
else:
for change in impl.diff(other_impl):
print(change)
# writer.emit(change)
else:
raise NotImplementedError


@main.command()
@click.option("--output", "-o")
@output_type_option
@click.argument("commands", nargs=-1)
def apply(commands, output, output_type):
"""
Applies a patch to an ontology
Example:
runoak -i cl.owl.ttl apply "rename CL:0000561 to 'amacrine neuron'" -o cl.owl.ttl -O ttl
With URIs:
runoak -i cl.owl.ttl apply \
"rename <http://purl.obolibrary.org/obo/CL_0000561> from 'amacrine cell' to 'amacrine neuron'" \
-o cl.owl.ttl -O ttl
WARNING:
This command is still experimental. Some things to bear in mind:
- for some ontologies, CURIEs may not work, instead specify a full URI surrounded by <>s
"""
impl = settings.impl
if isinstance(impl, PatcherInterface):
impl.autosave = settings.autosave
for command in commands:
change = kgcl_parser.parse_statement(command)
logging.info(f"Change: {change}")
impl.apply_patch(change)
if not settings.autosave and not output:
logging.warning("--autosave not passed, changes are NOT saved")
if output:
impl.dump(output, output_type)
else:
raise NotImplementedError

Expand All @@ -2188,7 +2296,7 @@ def diff_ontologies(output, output_type, other_ontology):
@click.option("--output", "-o")
@output_type_option
@click.argument("terms", nargs=-1)
def set_obsolete(output, output_type, terms):
def apply_obsolete(output, output_type, terms):
"""
Sets an ontology element to be obsolete
Expand All @@ -2197,12 +2305,12 @@ def set_obsolete(output, output_type, terms):
Example:
runoak -i my.obo set-obsolete MY:0002200 -o my-modified.obo
runoak -i my.obo apply-obsolete MY:0002200 -o my-modified.obo
This may be chained, for example to take all terms matching a search query and then
obsolete them all:
runoak -i my.db search 'l/^Foo/` | runoak -i my.db --autosave set-obsolete -
runoak -i my.db search 'l/^Foo/` | runoak -i my.db --autosave apply-obsolete -
"""
impl = settings.impl
if isinstance(impl, PatcherInterface):
Expand Down
1 change: 1 addition & 0 deletions src/oaklib/datamodels/vocabulary.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,4 @@
ALL_MATCH_PREDICATES = SKOS_MATCH_PREDICATES + [HAS_DBXREF, OWL_SAME_AS]
HAS_DEFINITION_URI = omd.slots.definition.uri
HAS_DEFINITION_CURIE = omd.slots.definition.curie
SKOS_DEFINITION_CURIE = "skos:definition"
2 changes: 1 addition & 1 deletion src/oaklib/implementations/funowl/funowl_implementation.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def get_label_by_curie(self, curie: CURIE) -> str:
else:
raise ValueError(f"Label must be literal, not {label}")

def all_entity_curies(self) -> Iterable[CURIE]:
def all_entity_curies(self, filter_obsoletes=True, owl_type=None) -> Iterable[CURIE]:
for ax in self._ontology.axioms:
if isinstance(ax, Declaration):
uri = ax.v.full_uri(self.functional_writer.g)
Expand Down
10 changes: 10 additions & 0 deletions src/oaklib/implementations/pronto/pronto_implementation.py
Original file line number Diff line number Diff line change
Expand Up @@ -489,5 +489,15 @@ def apply_patch(self, patch: kgcl.Change) -> None:
elif isinstance(patch, kgcl.NodeObsoletion):
t = self._entity(patch.about_node, strict=True)
t.obsolete = True
elif isinstance(patch, kgcl.NodeDeletion):
t = self._entity(patch.about_node, strict=True)
raise NotImplementedError
elif isinstance(patch, kgcl.NodeCreation):
self.create_entity(patch.about_node, patch.name)
elif isinstance(patch, kgcl.SynonymReplacement):
t = self._entity(patch.about_node, strict=True)
for syn in t.synonyms:
if syn.description == patch.old_value:
syn.description = patch.new_value
else:
raise NotImplementedError
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@
from dataclasses import dataclass, field
from typing import Dict, Iterable, Iterator, List, Optional, Tuple, Union

import kgcl_rdflib.apply.graph_transformer as kgcl_patcher
import kgcl_rdflib.kgcl_diff as kgcl_diff
import rdflib
import SPARQLWrapper
import sssom
from kgcl_schema.datamodel.kgcl import Change
from rdflib import RDFS, BNode, Literal, URIRef
from rdflib.term import Identifier
from SPARQLWrapper import JSON
Expand Down Expand Up @@ -34,6 +37,7 @@
PRED_CURIE,
PREFIX_MAP,
RELATIONSHIP_MAP,
BasicOntologyInterface,
)
from oaklib.interfaces.rdf_interface import TRIPLE, RdfInterface
from oaklib.resource import OntologyResource
Expand Down Expand Up @@ -421,12 +425,8 @@ def create_entity(
def add_relationship(self, curie: CURIE, predicate: PRED_CURIE, filler: CURIE):
raise NotImplementedError

def get_definition_by_curie(self, curie: CURIE) -> str:
"""
:param curie:
:return:
"""
def get_definition_by_curie(self, curie: CURIE) -> Optional[str]:
# TODO: allow this to be configured to use different predicates
labels = self._get_anns(curie, HAS_DEFINITION_URI)
if labels:
if len(labels) > 1:
Expand Down Expand Up @@ -644,6 +644,26 @@ def migrate_curies(self, curie_map: Dict[CURIE, CURIE]) -> None:
)
self._sparql_update(q)

def apply_patch(self, patch: Change) -> None:
if self.graph:
logging.info(f"Applying: {patch} to {self.graph}")
kgcl_patcher.apply_patch([patch], self.graph)
else:
raise NotImplementedError("Apply patch is only implemented for local graphs")

def diff(self, other_ontology: BasicOntologyInterface) -> Iterator[Change]:
if self.graph:
if isinstance(other_ontology, AbstractSparqlImplementation):
if other_ontology.graph:
for change in kgcl_diff.diff(self.graph, other_ontology.graph):
yield change
else:
raise NotImplementedError("Diff is only implemented for local graphs")
else:
raise NotImplementedError("Second ontology must implement sparql interface")
else:
raise NotImplementedError("Diff is only implemented for local graphs")

def save(self):
if self.graph:
if self.resource.format:
Expand Down
2 changes: 2 additions & 0 deletions src/oaklib/implementations/sparql/sparql_implementation.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from oaklib.implementations.sparql.abstract_sparql_implementation import (
AbstractSparqlImplementation,
)
from oaklib.interfaces.differ_interface import DifferInterface
from oaklib.interfaces.mapping_provider_interface import MappingProviderInterface
from oaklib.interfaces.obograph_interface import OboGraphInterface
from oaklib.interfaces.patcher_interface import PatcherInterface
Expand All @@ -14,6 +15,7 @@
@dataclass
class SparqlImplementation(
AbstractSparqlImplementation,
DifferInterface,
SearchInterface,
MappingProviderInterface,
OboGraphInterface,
Expand Down
6 changes: 3 additions & 3 deletions src/oaklib/interfaces/differ_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ class DifferInterface(BasicOntologyInterface, ABC):
TBD: low level diffs vs high level
See `KGCL <https://github.com/cmungall/knowledge-graph-change-language>`_
See `KGCL <https://github.com/INCATools/kgcl>`_
"""

def diff(self, other_ontology: BasicOntologyInterface) -> Any:
def diff(self, other_ontology: BasicOntologyInterface) -> Iterator[Change]:
"""
Diffs two ontologies
Expand All @@ -31,7 +31,7 @@ def compare_ontology_term_lists(
self, other_ontology: BasicOntologyInterface
) -> Iterator[Change]:
"""
Diffs two ontologies
Provides high level summary of differences
:param other_ontology:
:return:
Expand Down
10 changes: 7 additions & 3 deletions src/oaklib/interfaces/owl_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class ReasonerConfiguration:
@dataclass
class AxiomFilter:
type: Optional[Type[Axiom]] = None
about: Optional[CURIE] = None
about: Optional[Union[CURIE, List[CURIE]]] = None
references: Optional[CURIE] = None
func: Callable = None

Expand Down Expand Up @@ -209,8 +209,12 @@ def _axiom_matches(self, axiom: Axiom, conditions: AxiomFilter) -> bool:
if not isinstance(axiom, conditions.type):
return False
if conditions.about is not None:
if not any(e for e in self._axiom_is_about_curies(axiom) if e == conditions.about):
return False
if isinstance(conditions.about, list):
if not any(e for e in self._axiom_is_about_curies(axiom) if e in conditions.about):
return False
else:
if not any(e for e in self._axiom_is_about_curies(axiom) if e == conditions.about):
return False
if conditions.references is not None:
if not any(
e for e in self._axiom_references_curies(axiom) if e == conditions.references
Expand Down
2 changes: 1 addition & 1 deletion src/oaklib/interfaces/patcher_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class PatcherInterface(BasicOntologyInterface, ABC):
"""
Applies diffs
See `KGCL <https://github.com/cmungall/knowledge-graph-change-language>`_
See `KGCL <https://github.com/INCATools/kgcl>`_
"""

def apply_patch(self, patch: Change) -> None:
Expand Down
Loading

0 comments on commit 3420110

Please sign in to comment.