Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for external ontologies (dev-512) #170

Merged
merged 16 commits into from Apr 11, 2022
18 changes: 9 additions & 9 deletions docs/dsp-tools-create-ontologies.md
Expand Up @@ -891,19 +891,19 @@ Example for a resource definition:

## Referencing Ontologies

For several fields, such as `super` in both `resources` and `properties` or `propname` in `cardinalities`,
it is necessary to reference entities that are defined elsewhere. The following cases are possible.
For several fields, such as `super` in both `resources` and `properties` or `propname` in `cardinalities`
it is necessary to reference entities that are defined elsewhere. The following cases are possible:

- DSP-API internals: These must be written *without* leading colon and should not be a fully qualified IRI.
- DSP API internals: They are referenced as such and do not have a leading colon.
E.g. `Resource`, `DocumentRepresentation` or `hasValue`
- An external ontology: The ontology must be defined in the [prefixes](dsp-tools-create.md#prefixes-object)
This prefix should be used for referencing the ontology.
- An external ontology: The ontology must be defined in the [prefixes](dsp-tools-create.md#prefixes-object) section.
The prefix can then be used for referencing the ontology.
E.g. `foaf:familyName` or `sdo:Organization`
- The current ontology: Within an ontology definition, references can be made by prepending a colon without a prefix.
E.g. `:hasName`
Optionally, an explicit prefix can be used, in this case the ontology must be added to the
[prefixes](dsp-tools-create.md#prefixes-object) and the prefix must be identical to the ontology's `name`.
E.g. `:hasName`
Optionally, an explicit prefix can be used. In this case the ontology must be added to the
[prefixes](dsp-tools-create.md#prefixes-object) section and the prefix must be identical to the ontology's `name`.
- A different ontology defined in the same file: Within one data model file, multiple ontologies can be defined.
These will be created in the exact order they appear in the `ontologies` array. Once an ontology has been created,
it can be referenced by the following ontologies via its name: `first-onto:hasName`. It is not necessary to add
it can be referenced by the following ontologies by its name, p.ex. `first-onto:hasName`. It is not necessary to add
irinaschubert marked this conversation as resolved.
Show resolved Hide resolved
`first-onto` to the prefixes.
13 changes: 7 additions & 6 deletions docs/dsp-tools-create.md
Expand Up @@ -52,10 +52,10 @@ A complete data model definition for DSP looks like this:

`"prefixes": { "prefix": "<iri>", ...}`

The `prefixes` object contains the prefixes of external ontologies that are used in the current project. All prefixes
are composed of the actual prefix and an IRI. The prefix is used as an abbreviation so one does not have to write the
full qualified IRI each time it is used. So, instead of writing a property called "familyname" as
`http://xmlns.com/foaf/0.1/familyName` one can simply use `foaf:familyName`.
The `prefixes` object contains the prefixes of external ontologies that are used in the current file. All prefixes
are composed of the prefix and a URI. The prefix is used as namespace so one does not have to write the
fully qualified name of the referenced object each time it is used. Instead of writing a property called "familyName"
as `http://xmlns.com/foaf/0.1/familyName` one can simply write `foaf:familyName`.

```json
{
Expand All @@ -66,8 +66,9 @@ full qualified IRI each time it is used. So, instead of writing a property calle
}
```

It is not necessary to define prefixes for the ontologies that are defined in this file. Ontologies in the same
file can refer to each other via their name. See also [here](./dsp-tools-create-ontologies.md#referencing-ontologies).
It is not necessary to define prefixes for the ontologies that are defined in the same file. Ontologies in the same
file can be referenced by their name. See [this section](./dsp-tools-create-ontologies.md#referencing-ontologies) for
more information about referencing ontologies.

### "$schema" object

Expand Down
25 changes: 12 additions & 13 deletions knora/dsplib/models/connection.py
@@ -1,6 +1,6 @@
import json
import re
from typing import Optional, Union
from typing import Optional, Union, Any

import requests
from pystrict import strict
Expand Down Expand Up @@ -159,7 +159,7 @@ def post(self, path: str, jsondata: Optional[str] = None):
result = req.json()
return result

def get(self, path: str, headers: Optional[dict[str, str]] = None):
def get(self, path: str, headers: Optional[dict[str, str]] = None) -> dict[str, Any]:
"""
Get data from a server using a HTTP GET request
:param path: Path of RESTful route
Expand All @@ -169,22 +169,21 @@ def get(self, path: str, headers: Optional[dict[str, str]] = None):

if path[0] != '/':
path = '/' + path
if self._token is None:
if headers is None:
req = requests.get(self._server + path)
if not self._token:
if not headers:
response = requests.get(self._server + path)
else:
req = requests.get(self._server + path, headers)
response = requests.get(self._server + path, headers)
else:
if headers is None:
req = requests.get(self._server + path,
headers={'Authorization': 'Bearer ' + self._token})
if not headers:
response = requests.get(self._server + path, headers={'Authorization': 'Bearer ' + self._token})
else:
headers['Authorization'] = 'Bearer ' + self._token
req = requests.get(self._server + path, headers)
response = requests.get(self._server + path, headers)

self.on_api_error(req)
result = req.json()
return result
self.on_api_error(response)
json_response = response.json()
return json_response

def put(self, path: str, jsondata: Optional[str] = None, content_type: str = 'application/json'):
"""
Expand Down
24 changes: 11 additions & 13 deletions knora/dsplib/models/group.py
Expand Up @@ -160,8 +160,8 @@ def has_changed(self) -> bool:

@classmethod
def fromJsonObj(cls, con: Connection, json_obj: Any):
id = json_obj.get('id')
if id is None:
group_id = json_obj.get('id')
if group_id is None:
raise BaseError('Group "id" is missing')
name = json_obj.get('name')
if name is None:
Expand All @@ -181,7 +181,7 @@ def fromJsonObj(cls, con: Connection, json_obj: Any):
raise BaseError("Status is missing")
return cls(con=con,
name=name,
id=id,
id=group_id,
descriptions=descriptions,
project=project,
selfjoin=selfjoin,
Expand Down Expand Up @@ -249,16 +249,14 @@ def getAllGroups(con: Connection) -> Optional[list[Group]]:
return None

@staticmethod
def getAllGroupsForProject(con: Connection, proj_shortcode: str) -> list[Group]:
result = con.get(Group.ROUTE)
if 'groups' not in result:
raise BaseError("Request got no groups!")
all_groups = result["groups"]
project_groups = []
for group in all_groups:
if group["project"]["id"] == "http://rdfh.ch/projects/" + proj_shortcode:
project_groups.append(group)
return list(map(lambda a: Group.fromJsonObj(con, a), project_groups))
def getAllGroupsForProject(con: Connection, proj_shortcode: str) -> Optional[list[Group]]:
all_groups: Optional[list[Group]] = Group.getAllGroups(con)
if all_groups:
project_groups = []
for group in all_groups:
if group.project == "http://rdfh.ch/projects/" + proj_shortcode:
project_groups.append(group)
return project_groups
irinaschubert marked this conversation as resolved.
Show resolved Hide resolved

def createDefinitionFileObj(self):
group = {
Expand Down
106 changes: 58 additions & 48 deletions knora/dsplib/models/helpers.py
Expand Up @@ -16,8 +16,11 @@
@dataclass
class OntoInfo:
irinaschubert marked this conversation as resolved.
Show resolved Hide resolved
"""
A small class thats holds an ontology IRI. The variable "hashtag" is True, if "#" is used s separate elements,
False if the element name is just appended
Holds an ontology IRI

Attributes:
iri: the ontology IRI
hashtag: True if "#" is used to separate elements, False if element name is appended after "/"
"""
iri: str
hashtag: bool
Expand Down Expand Up @@ -122,7 +125,10 @@ class Context:
"skos": OntoInfo("http://www.w3.org/2004/02/skos/core", True),
"bibtex": OntoInfo("http://purl.org/net/nknouf/ns/bibtex", True),
"bibo": OntoInfo("http://purl.org/ontology/bibo/", False),
"cidoc": OntoInfo("http://purl.org/NET/cidoc-crm/core", True)
"cidoc": OntoInfo("http://purl.org/NET/cidoc-crm/core", True),
"schema": OntoInfo("https://schema.org/", False),
"edm": OntoInfo("http://www.europeana.eu/schemas/edm/", False),
"ebucore": OntoInfo("http://www.ebu.ch/metadata/ontologies/ebucore/ebucore", True)
})

knora_ontologies = ContextType({
Expand Down Expand Up @@ -242,8 +248,6 @@ def add_context(self, prefix: str, iri: Optional[str] = None) -> None:
return
if prefix in self.common_ontologies:
self._context[prefix] = self.common_ontologies[prefix]
else:
raise BaseError("The prefix '{}' is not known!".format(prefix))
irinaschubert marked this conversation as resolved.
Show resolved Hide resolved
else:
if iri.endswith("#"):
iri = iri[:-1]
Expand Down Expand Up @@ -303,15 +307,16 @@ def prefix_from_iri(self, iri: str) -> Optional[str]:

def get_qualified_iri(self, val: Optional[str]) -> Optional[str]:
"""
We will return the full qualified IRI, if it is not yet a full qualified IRI. If
the IRI is already fully qualified, the we just return it.
Given an IRI, its fully qualified name is returned.

Args:
val: The input IRI

:param val: The input short form
:return: the fully qualified IRI
Returns:
the fully qualified IRI
"""
if val is None:
if not val:
return None
# if self.__is_iri(val):
if IriTest.test(val):
return val
tmp = val.split(':')
Expand All @@ -326,7 +331,7 @@ def get_qualified_iri(self, val: Optional[str]) -> Optional[str]:
self._rcontext[entry[1].iri] = entry[0]
iri_info = entry[1]
else:
raise BaseError("Ontology not known! Cannot generate full qualified IRI")
raise BaseError("Ontology not known! Cannot generate fully qualified IRI")
if iri_info.hashtag:
return iri_info.iri + '#' + tmp[1]
else:
Expand All @@ -339,66 +344,67 @@ def get_prefixed_iri(self, iri: Optional[str]) -> Optional[str]:
:param iri: Fully qualified IRI
:return: Return short from of IRI ("prefix:name")
"""

if iri is None:
return None
#

# check if the iri already has the form "prefix:name"
#
m = re.match("([\\w-]+):([\\w-]+)", iri)
if m and m.span()[1] == len(iri):
return iri
# if not self.__is_iri(iri):

if not IriTest.test(iri):
raise BaseError("String does not conform to IRI patter: " + iri)
raise BaseError(f"The IRI '{iri}' does not conform to the IRI pattern.")

splitpoint = iri.find('#')
if splitpoint == -1:
splitpoint = iri.rfind('/')
ontopart = iri[:splitpoint + 1]
element = iri[splitpoint + 1:]
split_point = iri.find('#')
if split_point == -1:
split_point = iri.rfind('/')
onto_part = iri[:split_point + 1]
element = iri[split_point + 1:]
else:
ontopart = iri[:splitpoint]
element = iri[splitpoint + 1:]
prefix = self._rcontext.get(ontopart)
onto_part = iri[:split_point]
element = iri[split_point + 1:]
irinaschubert marked this conversation as resolved.
Show resolved Hide resolved

prefix = self._rcontext.get(onto_part)
if prefix is None:
entrylist = list(filter(lambda x: x[1].iri == ontopart, self.common_ontologies.items()))
if len(entrylist) == 1:
entry = entrylist[0]
entry_list = list(filter(lambda x: x[1].iri == onto_part, self.common_ontologies.items()))
irinaschubert marked this conversation as resolved.
Show resolved Hide resolved
if len(entry_list) == 1:
entry = entry_list[0]
self._context[entry[0]] = entry[1] # add to list of prefixes used
self._rcontext[entry[1].iri] = entry[0]
prefix = entry[0]
else:
raise BaseError(
"Ontology {} not known! Cannot generate full qualified IRI: prefix={}".format(iri, prefix))
return None
return prefix + ':' + element

def reduce_iri(self, iristr: str, ontoname: Optional[str] = None) -> str:
def reduce_iri(self, iri_str: str, onto_name: Optional[str] = None) -> str:
"""
Reduce an IRI to the form that is used within the definition json file. It expects
the context object to have entries (prefixes) for all IRI's
- if it's an external IRI, it returns: "prefix:name"
- if it's in the same ontology, it returns ":name"
- if it's a system ontoloy ("knora-api" or "salsah-gui") it returns "name"
:param iristr:
:return:
Reduces an IRI to the form that is used within the definition JSON file. It expects the context object to have
entries (prefixes) for all IRIs:
- if it's an external IRI and the ontology can be extracted as prefix it returns: "prefix:name"
- if it's in the same ontology, it returns: ":name"
- if it's a system ontology ("knora-api" or "salsah-gui") it returns: "name"
- if the IRI can't be reduced, it's returned as is

Args:
iri_str: the IRI that should be reduced
onto_name: the name of the ontology

Returns:
The reduced IRI if possible otherwise the fully qualified IRI
"""
rdf = self.prefix_from_iri("http://www.w3.org/1999/02/22-rdf-syntax-ns#")
rdfs = self.prefix_from_iri("http://www.w3.org/2000/01/rdf-schema#")
owl = self.prefix_from_iri("http://www.w3.org/2002/07/owl#")
xsd = self.prefix_from_iri("http://www.w3.org/2001/XMLSchema#")
knora_api = self.prefix_from_iri("http://api.knora.org/ontology/knora-api/v2#")
salsah_gui = self.prefix_from_iri("http://api.knora.org/ontology/salsah-gui/v2#")

if IriTest.test(iristr):
iristr = self.get_prefixed_iri(iristr)
tmp = iristr.split(':')
if IriTest.test(iri_str):
if self.get_prefixed_iri(iri_str):
iri_str = self.get_prefixed_iri(iri_str)
tmp = iri_str.split(':')
if tmp[0] == knora_api or tmp[0] == salsah_gui:
return tmp[1]
elif ontoname is not None and tmp[0] == ontoname:
elif onto_name and tmp[0] == onto_name:
irinaschubert marked this conversation as resolved.
Show resolved Hide resolved
return ':' + tmp[1]
else:
return iristr
return iri_str

def toJsonObj(self) -> dict[str, str]:
"""
Expand All @@ -410,7 +416,11 @@ def toJsonObj(self) -> dict[str, str]:

def get_externals_used(self) -> dict[str, str]:
exclude = ["rdf", "rdfs", "owl", "xsd", "knora-api", "salsah-gui"]
return {prefix: onto.iri for prefix, onto in self._context.items() if prefix not in exclude}
prefixes: dict[str, str] = {}
for prefix, onto in self._context.items():
if prefix not in exclude:
prefixes[prefix] = onto.iri
return prefixes
irinaschubert marked this conversation as resolved.
Show resolved Hide resolved

def print(self) -> None:
for a in self._context.items():
Expand Down
2 changes: 1 addition & 1 deletion knora/dsplib/models/propertyclass.py
Expand Up @@ -417,7 +417,7 @@ def delete(self, last_modification_date: LastModificationDate) -> LastModificati

def createDefinitionFileObj(self, context: Context, shortname: str):
"""
Create an object that jsonfied can be used as input to "create_onto"
Create an object that can be used as input to create an ontology on a DSP server
irinaschubert marked this conversation as resolved.
Show resolved Hide resolved

:param context: Context of the ontology
:param shortname: Shortname of the ontology
Expand Down