Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
43613e8
add yml-specs for datamode, dcterms, prov, dcmitype. Update for dcat2
MBueschelberger Feb 14, 2022
519f1ff
update dcat2, dcmitype and dcterms ontology. Add RDFS.Class as availa…
MBueschelberger Feb 14, 2022
5fd1528
update yml-spec for datamodel
MBueschelberger Feb 17, 2022
b51037e
Merge branch 'dev' into enh/add-dcat-and-datamodel
MBueschelberger Mar 1, 2022
7604d37
Fix ontology2dot.
domgz Mar 17, 2022
9361a66
Merge remote-tracking branch 'origin/enh/add-dcat-and-datamodel' into…
domgz Mar 17, 2022
69159d2
Validate ontology files before installation and warn about unsupporte…
domgz Mar 17, 2022
224a7e9
Fix flake8.
domgz Mar 17, 2022
a4766a4
Merge branch 'dev' into enh/add-dcat-and-datamodel
domgz Mar 17, 2022
bc52700
Fix tests.
domgz Mar 17, 2022
8d69627
Rename EMMO datamodel package and namespace.
domgz Mar 17, 2022
e342915
Reorganize names of ontology packages again.
domgz Mar 17, 2022
6e0c153
Fix problems with warnings.
domgz Mar 17, 2022
58980e9
Fix flake8.
domgz Mar 17, 2022
6b25fe0
Review active relationships for dcat2.yml.
domgz Mar 18, 2022
db49c43
Review active relationships for emmo-datamodel.yml. Keep only mereolo…
domgz Mar 18, 2022
fe8aa78
Review prov.yml.
domgz Mar 18, 2022
5cbb21b
Fix problems with Python 3.6.
domgz Mar 18, 2022
895912a
Add automatic resolution of dependencies.
domgz Mar 24, 2022
8ae35e7
Show installation logs when pico is used as a module.
domgz Mar 24, 2022
cead7af
Fix tests.
domgz Mar 24, 2022
dff2b73
Fix flake8.
domgz Mar 24, 2022
ca826ce
Try to fix tests again for Python 3.6.
domgz Mar 24, 2022
43e64b8
Fix bug with prov on Python 3.6 (format not guessed by RDFLib 5.0.0 b…
domgz Mar 24, 2022
9e4949d
Code review changes.
domgz Mar 25, 2022
f5b2612
Fix bug that ignored custom files for dependencies provided by the user.
domgz Mar 25, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion osp/core/ontology/docs/dcat2.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@
identifier: dcat2
ontology_file: https://www.w3.org/ns/dcat2.rdf
reference_by_label: False
requirements:
- dcterms
- prov
- foaf
namespaces:
dcat2: http://www.w3.org/ns/dcat#
active_relationships: []

active_relationships:
Comment thread
domgz marked this conversation as resolved.
- http://www.w3.org/ns/dcat#catalog
- http://www.w3.org/ns/dcat#dataset
- http://www.w3.org/ns/dcat#service
- http://www.w3.org/ns/dcat#record
6 changes: 6 additions & 0 deletions osp/core/ontology/docs/dcmitype.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
identifier: dcmitype
ontology_file: https://www.dublincore.org/specifications/dublin-core/dcmi-terms/dublin_core_type.ttl
reference_by_label: False
namespaces:
dcmitype: "http://purl.org/dc/dcmitype/"
active_relationships: []
9 changes: 9 additions & 0 deletions osp/core/ontology/docs/dcterms.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
identifier: dcterms
ontology_file: https://www.dublincore.org/specifications/dublin-core/dcmi-terms/dublin_core_terms.ttl
reference_by_label: False
requirements:
- dcmitype
namespaces:
dcterms: "http://purl.org/dc/terms/"
active_relationships: []
7 changes: 7 additions & 0 deletions osp/core/ontology/docs/emmo-datamodel.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
identifier: emmo-datamodel
ontology_file: https://emmo-repo.github.io/datamodel-ontology/versions/0.0.2/metamodel-inferred.ttl
reference_by_label: Treu
namespaces:
datamodel: http://emmo.info/datamodel#
active_relationships:
- "http://emmo.info/datamodel#composition"
8 changes: 8 additions & 0 deletions osp/core/ontology/docs/prov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
identifier: prov
ontology_file: http://www.w3.org/ns/prov-o
format: ttl
reference_by_label: False
namespaces:
prov: http://www.w3.org/ns/prov#
active_relationships: []
210 changes: 207 additions & 3 deletions osp/core/ontology/installation.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
import shutil
import sys
import tempfile
from typing import Dict, Set

from osp.core.ontology.parser.parser import OntologyParser

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)


class OntologyInstallationManager:
Expand Down Expand Up @@ -120,7 +122,107 @@ def _get_remaining_packages(self, remove_packages):
"installed ontology package. "
"Make sure to only specify valid "
"yml files or ontology package names." % pkg)
return [v for k, v in installed_pkgs.items() if k not in remove_pkgs]

remaining_packages = {
k: v for k, v in installed_pkgs.items() if k not in remove_pkgs
}

# Block package removal if another package depends on it.
remaining_packages_requirements = {
name: OntologyParser.get_parser(path).requirements
for name, path in remaining_packages.items()
}
all_conflicts = self._resolve_dependencies_removal(
remaining_packages_requirements,
dict(),
set(remove_pkgs)
)
if all_conflicts:
"""Raise an exception."""
message = "Cannot remove package{plural} {cannot_remove}{comma} " \
"because other installed packages depend on {pronoun}: "\
"{dependency_list}. " \
"Please remove the packages {all_packages_to_remove} " \
"all together."
cannot_remove = set(conflict
for conflicts in all_conflicts.values()
for conflict in conflicts
if conflict in remove_pkgs)
plural = "s" if len(cannot_remove) > 1 else ""
comma = ";" if plural else ","
pronoun = "them" if plural else "it"
cannot_remove = ', '.join(cannot_remove)
one_dependency = next(iter(all_conflicts.values()))
all_dependencies_equal = all(
one_dependency == x for x in all_conflicts.values()
)
if all_dependencies_equal:
dependency_list = ', '.join(all_conflicts)
else:
dependency_list = set()
for package, conflicts in all_conflicts.items():
dependency_list.add(f"package {package} depends on "
f"{', '.join(conflicts)}")
dependency_list = '; '.join(dependency_list)
all_packages_to_remove = set(all_conflicts) | remove_pkgs
last_package = all_packages_to_remove.pop()
all_packages_to_remove = ', '.join(
all_packages_to_remove
) + ' and ' + last_package
message = message.format(
plural=plural, cannot_remove=cannot_remove,
comma=comma, pronoun=pronoun, dependency_list=dependency_list,
all_packages_to_remove=all_packages_to_remove,
)
raise RuntimeError(message)

return [v for v in remaining_packages.values()]

def _resolve_dependencies_removal(
self,
remaining_packages_requirements: Dict[str, Set[str]],
all_conflicts: Dict[str, Set[str]],
to_remove: Set[str],
) -> Dict[str, Set[str]]:
"""Resolve the dependencies when a package is removed.

Finds out (using recursive calls) all the packages that would need
to be removed together with the packages that want to be removed in
order not to leave broken dependencies.

Args:
remaining_packages_requirements: The requirements of the
packages that would remain installed after uninstalling the
packages specified in `to_remove`.
all_conflicts: All the conflicts that accumulate as the
function is called recursively. At the end, a list of all
the packages that would need to be removed to leave the
system in a healthy state can be reconstructed from it.
to_remove: The packages that are to be removed.
"""
conflicts: Dict[str, Set[str]] = {
name: requirements & to_remove
for name, requirements in remaining_packages_requirements.items()
}
conflicts = {
name: conflicts
for name, conflicts in conflicts.items() if conflicts
}
all_conflicts.update(conflicts)
to_remove.update(conflicts)
remaining_packages_requirements = {
package: requirements
for package, requirements
in remaining_packages_requirements.items()
if package not in to_remove
}
if conflicts:
self._resolve_dependencies_removal(
remaining_packages_requirements,
all_conflicts,
to_remove
)
return all_conflicts

def _get_replaced_packages(self, new_packages):
"""Get package paths to install.
Expand Down Expand Up @@ -213,8 +315,8 @@ def _install(self, files, filter_func, clear):
for file in files_to_remove:
os.remove(os.path.join(self.path, file))
if python_36: # Bound and unbound namespaces manually
import osp.core as core
import osp.core.namespaces as namespaces
from ... import core
from .. import namespaces
if unbound_manually:
unbound_manually = unbound_manually.difference(
ns for ns in self.namespace_registry
Expand Down Expand Up @@ -242,6 +344,25 @@ def _sort_for_installation(self, files, installed):
requirements = {n: OntologyParser.get_parser(f).requirements for
n, f in files.items()}

# If the requirements for an ontology package are bundled with
# OSP-core, try to install them automatically.
package_and_dependents = dict()
try:
package_and_dependents: Dict[str, Set[str]] = \
self._resolve_dependencies_install(
files, requirements, dict()
)
files.update({
OntologyParser.get_parser(f).identifier: f
for f in package_and_dependents
})
requirements.update({
n: OntologyParser.get_parser(f).requirements
for n, f in files.items()
})
except FileNotFoundError:
pass

# order the files
while requirements:
add_to_result = list()
Expand All @@ -258,10 +379,93 @@ def _sort_for_installation(self, files, installed):
result += add_to_result
for x in add_to_result:
del requirements[x]
dependencies_to_install = set(package_and_dependents) - set(installed)
if dependencies_to_install:
logger.info(
"Also installing dependencies: %s."
% ', '.join(dependencies_to_install)
)
logger.info("Will install the following namespaces: %s"
% result)
return [files[n] for n in result]

def _resolve_dependencies_install(self,
files: Dict[str, str],
requirements: Dict[str, Set[str]],
dependents: Dict[str, Set[str]]) -> \
Dict[str, Set[str]]:
"""Find and resolve the dependencies of the packages to be installed.

Automatic resolution of dependencies is only feasible if the
dependency is bundled with OSP-core.

Args:
files: The packages that are going to be installed together with
their file path.
requirements: The dependencies for each package that is going to
be installed.
dependents: A dictionary with package names and the packages
that depend on them.
"""
initial_files = files
additional_files: Dict[str, str] = dict()
# The statement below avoids installing the file bundled with
# OSP-core when the user provides a custom file providing the same
# package identifier.
requirements = {
n: {req for req in requirements_set
if req not in initial_files}
for n, requirements_set in requirements.items()
}
new_requirements: Dict[str, Set[str]] = dict()
for package, requirements_set in requirements.items():
# Queue the requirements for installation if bundled with
# OSP-core and not already queued.
actually_missing_requirements = {
package
for package in requirements_set
if package not in additional_files
}
for requirement in actually_missing_requirements:
try:
parser = OntologyParser.get_parser(requirement)
additional_files[parser.identifier] = requirement
new_requirements.update({
parser.identifier: parser.requirements
})
except FileNotFoundError:
pass

# Store which packages are requiring the requirements that were
# queued for installation to show the information on the logs.
# In addition, the `dependents` dictionary keys are the
# additional packages to be installed.
initially_missing_requirements = {
package
for package in requirements_set
if package not in initial_files
}
for requirement in initially_missing_requirements:
dependents[requirement] = dependents.get(
requirement, set()) | {package}
files.update(additional_files)
new_requirements_exist = bool(
{req
for req_set in new_requirements.values()
for req in req_set}
- {req
for req_set in requirements.values()
for req in req_set}
)
if new_requirements_exist:
requirements.update(new_requirements)
dependents = self._resolve_dependencies_install(
files,
requirements,
dependents
)
return dependents


def pico_migrate(namespace_registry, path):
"""Migrate old installations to new.
Expand Down
3 changes: 2 additions & 1 deletion osp/core/ontology/namespace.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,8 @@ def _iter_iris(self):
"""
types = [rdflib.OWL.DatatypeProperty,
rdflib.OWL.ObjectProperty,
rdflib.OWL.Class]
rdflib.OWL.Class,
rdflib.RDFS.Class]
return (s
for t in types
for s, _, _ in self._graph.triples((None, rdflib.RDF.type, t))
Expand Down
5 changes: 3 additions & 2 deletions osp/core/ontology/namespace_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,8 @@ def namespace_from_iri(self, ns_iri):
def from_iri(self, iri, raise_error=True,
allow_types=frozenset({rdflib.OWL.DatatypeProperty,
rdflib.OWL.ObjectProperty,
rdflib.OWL.Class}),
rdflib.OWL.Class,
rdflib.RDFS.Class}),
_name=None):
"""Get an entity from IRI.

Expand Down Expand Up @@ -177,7 +178,7 @@ def from_iri(self, iri, raise_error=True,
return OntologyAttribute(**kwargs)
if o == rdflib.OWL.ObjectProperty:
return OntologyRelationship(**kwargs)
if o == rdflib.OWL.Class:
if o in (rdflib.OWL.Class, rdflib.RDFS.Class):
return OntologyClass(**kwargs)
if raise_error:
raise KeyError(f"IRI {iri} not found in graph or not of any "
Expand Down
Loading