Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 0 additions & 7 deletions builder/fairgraph_module_template.py.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,6 @@ class {{ class_name }}({{ base_class }}):
default_space = "{{ default_space }}"
{%- endif %}
type_ = "{{ openminds_type }}"
context = {
"schema": "http://schema.org/",
"kg": "https://kg.ebrains.eu/api/instances/",
"vocab": "https://openminds.ebrains.eu/vocab/",
"terms": "https://openminds.ebrains.eu/controlledTerms/",
"core": "https://openminds.ebrains.eu/core/"
}
properties = [
{% for prop in properties -%}
Property("{{prop.name}}", {{prop.type_str}}, "{{prop.iri}}",
Expand Down
98 changes: 60 additions & 38 deletions builder/update_openminds.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@
reverse_name_map = {
"RRID": "identifies",
"about": {
"https://openminds.ebrains.eu/publications/LearningResource": "learningResources",
"https://openminds.ebrains.eu/core/Comment": "comments",
"https://openminds.ebrains.eu/publications/LivePaperVersion": "publication",
"LearningResource": "learningResources",
"Comment": "comments",
"LivePaperVersion": "publication",
},
"abstractionLevel": "isAbstractionLevelOf",
"accessibility": "isAccessibilityOf",
Expand Down Expand Up @@ -82,16 +82,16 @@
"describedIn": "describes",
"developer": "developed",
"device": {
"https://openminds.ebrains.eu/ephys/ElectrodeArrayUsage": "usage",
"https://openminds.ebrains.eu/ephys/RecordingActivity": "usedIn",
"https://openminds.ebrains.eu/ephys/ElectrodePlacement": "placedBy",
"https://openminds.ebrains.eu/ephys/CellPatching": "usedIn",
"https://openminds.ebrains.eu/ephys/Recording": "usedFor",
"https://openminds.ebrains.eu/core/Measurement": "usedFor",
"https://openminds.ebrains.eu/specimenPrep/TissueSampleSlicing": "usedFor",
"https://openminds.ebrains.eu/specimenPrep/SlicingDeviceUsage": "usage",
"https://openminds.ebrains.eu/ephys/PipetteUsage": "usage",
"https://openminds.ebrains.eu/ephys/ElectrodeUsage": "usage",
"ElectrodeArrayUsage": "usage",
"RecordingActivity": "usedIn",
"ElectrodePlacement": "placedBy",
"CellPatching": "usedIn",
"Recording": "usedFor",
"Measurement": "usedFor",
"TissueSampleSlicing": "usedFor",
"SlicingDeviceUsage": "usage",
"PipetteUsage": "usage",
"ElectrodeUsage": "usage",
},
"deviceType": "isTypeOf", # TODO: replace with "type"?
"digitalIdentifier": "identifies",
Expand All @@ -112,8 +112,8 @@
"geneticStrainType": "isGeneticStrainTypeOf", # or "strain"
"groupedBy": "isUsedToGroup",
"groupingType": {
"https://openminds.ebrains.eu/core/FileBundle": "isUsedToGroup",
"https://openminds.ebrains.eu/core/FilePathPattern": "isDefinedBy",
"FileBundle": "isUsedToGroup",
"FilePathPattern": "isDefinedBy",
},
"handedness": "subjectStates", # or "isHandednessOf"
"hardware": "usedBy", # or "isPartOfEnvironment"
Expand Down Expand Up @@ -181,8 +181,8 @@
"scoreType": "isScoreTypeOf",
"serializationFormat": "usedBy",
"service": {
"https://openminds.ebrains.eu/core/ServiceLink": "linkedFrom",
"https://openminds.ebrains.eu/core/AccountInformation": "hasAccounts",
"ServiceLink": "linkedFrom",
"AccountInformation": "hasAccounts",
},
"setup": "usedIn",
"slicingDevice": "usedIn", # TODO: slicingDevice --> device?
Expand All @@ -209,9 +209,9 @@
"type": "isTypeOf",
"typeOfUncertainty": "value",
"unit": {
"https://openminds.ebrains.eu/ephys/Channel": "usedIn",
"https://openminds.ebrains.eu/core/QuantitativeValue": "value",
"https://openminds.ebrains.eu/core/QuantitativeValueArray": "value",
"Channel": "usedIn",
"QuantitativeValue": "value",
"QuantitativeValueArray": "value",
},
"usedSpecies": "commonCoordinateSpace",
"usedSpecimen": "usedIn",
Expand Down Expand Up @@ -411,7 +411,7 @@ def get_default_space(schema_group, cls_name):
custom_existence_queries = {
"LaunchConfiguration": ("executable", "name"),
"Person": ("given_name", "family_name"),
"File": ("iri", "hash"),
"File": ("iri", "hashes"),
"FileRepository": ("iri",),
"License": ("alias",),
"DOI": ("identifier",),
Expand Down Expand Up @@ -454,6 +454,7 @@ def get_default_space(schema_group, cls_name):
"Periodical": ("abbreviation",),
"AmountOfChemical": ("chemical_product", "amount"),
"QuantitativeValue": ("value", "unit", "uncertainties"),
"Hash": ("algorithm", "digest")
}


Expand Down Expand Up @@ -484,12 +485,11 @@ def property_name_sort_key(property_name):
return priorities.get(property_name, property_name)


def generate_class_name(iri):
def generate_class_name(iri, module_map=None):
assert isinstance(iri, str)
parts = iri.split("/")[-2:]
for i in range(len(parts) - 1):
parts[i] = generate_python_name(parts[i])
return "openminds." + ".".join(parts)
class_name = iri.split("/")[-1]
module_name = generate_python_name(module_map[iri])
return f"openminds.{module_name}.{class_name}"


def get_controlled_terms_table(type_):
Expand Down Expand Up @@ -557,7 +557,6 @@ def get_controlled_terms_table(type_):
preamble = {
"File": """import os
import mimetypes
from numbers import Real
from pathlib import Path
from urllib.request import urlretrieve
from urllib.parse import quote, urlparse, urlunparse
Expand Down Expand Up @@ -587,6 +586,17 @@ def get_controlled_terms_table(type_):
}


def get_type_from_schema(schema_payload, override=True):
if override: # temporarily use the old namespaces, until the KG is updated
cls_name = schema_payload["_type"].split("/")[-1]
module_name = schema_payload['_module']
if module_name == "SANDS":
module_name = "sands"
return f"https://openminds.ebrains.eu/{module_name}/{cls_name}"
else:
return schema_payload["_type"]


class FairgraphClassBuilder:
"""docstring"""

Expand All @@ -608,7 +618,7 @@ def __init__(self, schema_file_path: str, root_path: str, target_path_root: str)
def _target_file_without_extension(self) -> str:
return os.path.join(*self.relative_path_without_extension)

def translate(self, embedded=None, linked=None):
def translate(self, embedded=None, linked=None, module_map=None):
def get_type(prop):
type_map = {
"string": "str",
Expand All @@ -621,20 +631,21 @@ def get_type(prop):
"email": "str", # todo: add an Email class for validation?
"ECMA262": "str", # ...
}
#breakpoint()
if "_linkedTypes" in prop:
types = []
for item in prop["_linkedTypes"]:
openminds_module, class_name = item.split("/")[-2:]
openminds_module = generate_python_name(openminds_module)
class_name = item.split("/")[-1]
openminds_module = generate_python_name(module_map[item])
types.append(f"openminds.{openminds_module}.{class_name}")
if len(types) == 1:
types = f'"{types[0]}"'
return types
elif "_embeddedTypes" in prop:
types = []
for item in prop["_embeddedTypes"]:
openminds_module, class_name = item.split("/")[-2:]
openminds_module = generate_python_name(openminds_module)
class_name = item.split("/")[-1]
openminds_module = generate_python_name(module_map[item])
types.append(f"openminds.{openminds_module}.{class_name}")
if len(types) == 1:
types = f'"{types[0]}"'
Expand Down Expand Up @@ -709,7 +720,7 @@ def get_type(prop):
linked_from = linked[self._schema_payload["_type"]]
for reverse_link_name in linked_from:
unique_forward_iris = set(linked_from[reverse_link_name][0])
types_str = [generate_class_name(iri) for iri in linked_from[reverse_link_name][2]]
types_str = [generate_class_name(iri, module_map) for iri in linked_from[reverse_link_name][2]]
if len(unique_forward_iris) == 1:
(forward_iri,) = unique_forward_iris
forward_link_names = set(linked_from[reverse_link_name][1])
Expand Down Expand Up @@ -771,7 +782,7 @@ def get_type(prop):
"preamble": preamble.get(class_name, ""), # default value, may be updated below
"class_name": class_name,
"default_space": default_space,
"openminds_type": self._schema_payload["_type"],
"openminds_type": get_type_from_schema(self._schema_payload, override=True),
"properties": sorted(properties, key=lambda p: p["name"]),
"reverse_properties": sorted(reverse_properties, key=lambda p: p["name"]),
"additional_methods": "",
Expand All @@ -790,6 +801,7 @@ def get_type(prop):
"time": "from datetime import time",
"IRI": "from fairgraph.base import IRI",
"[datetime, time]": "from datetime import datetime, time",
"Real": "from numbers import Real"
}
extra_imports = set()
for prop in self.context["properties"]:
Expand All @@ -807,11 +819,11 @@ def get_type(prop):
if module_name == "controlled_terms":
self.context["docstring"] += get_controlled_terms_table(self._schema_payload["_type"])

def build(self, embedded=None, linked=None):
def build(self, embedded=None, linked=None, module_map=None):
target_file_path = os.path.join(self.target_path_root, f"{self._target_file_without_extension()}.py")
os.makedirs(os.path.dirname(target_file_path), exist_ok=True)

self.translate(embedded=embedded, linked=linked)
self.translate(embedded=embedded, linked=linked, module_map=module_map)

with open(target_file_path, "w") as target_file:
contents = self.env.get_template(self.template_name).render(self.context)
Expand Down Expand Up @@ -840,6 +852,9 @@ def get_edges(self):
# breakpoint()
return embedded, linked

def get_module_map(self):
return self._schema_payload["_type"], self._schema_payload["_module"]


def main(openminds_root, ignore=[]):
target_path = os.path.join("..", "fairgraph", "openminds")
Expand All @@ -850,6 +865,12 @@ def main(openminds_root, ignore=[]):
schema_file_paths = glob(os.path.join(openminds_root, f"**/*.schema.omi.json"), recursive=True)
python_modules = defaultdict(list)

# Zeroth pass - map schemas to modules
module_map = {}
for schema_file_path in schema_file_paths:
type_, module_name = FairgraphClassBuilder(schema_file_path, openminds_root, target_path).get_module_map()
module_map[type_] = module_name

# First pass - figure out which schemas are embedded and which are linked
embedded = set()
linked = defaultdict(dict)
Expand All @@ -859,7 +880,8 @@ def main(openminds_root, ignore=[]):
for openminds_type, (link_type, property_name, forward_name, reverse_name) in linked_from.items():
if link_type not in embedded:
if isinstance(reverse_name, dict):
reverse_name = reverse_name[link_type]
cls_name = link_type.split("/")[-1]
reverse_name = reverse_name[cls_name]
if reverse_name in linked[openminds_type]:
linked[openminds_type][reverse_name][0].append(property_name)
linked[openminds_type][reverse_name][1].append(forward_name)
Expand All @@ -876,7 +898,7 @@ def main(openminds_root, ignore=[]):
# Second pass - create a Python module for each openMINDS schema
for schema_file_path in schema_file_paths:
module_path, class_name = FairgraphClassBuilder(schema_file_path, openminds_root, target_path).build(
embedded=embedded, linked=linked
embedded=embedded, linked=linked, module_map=module_map
)

parts = module_path.split(".")
Expand Down
28 changes: 24 additions & 4 deletions fairgraph/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
as_list, # temporary for backwards compatibility (a lot of code imports it from here)
expand_uri,
normalize_data,
invert_dict
invert_dict,
types_match
)

if TYPE_CHECKING:
Expand All @@ -45,6 +46,15 @@

JSONdict = Dict[str, Any] # see https://github.com/python/typing/issues/182 for some possible improvements

default_context = {
"v3": {
"vocab": "https://openminds.ebrains.eu/vocab/",
},
"v4": {
"vocab": "https://openminds.om-i.org/props/"
}
}


class ErrorHandling(str, Enum):
error = "error"
Expand Down Expand Up @@ -76,7 +86,7 @@ def resolve(
class ContainsMetadata(Resolvable, metaclass=Registry): # KGObject and EmbeddedMetadata
properties: List[Property]
reverse_properties: List[Property]
context: Dict[str, str]
context: Dict[str, str] = default_context["v3"]
type_: str
scope: Optional[str]
space: Union[str, None]
Expand Down Expand Up @@ -322,7 +332,19 @@ def generate_query_filter_properties(

@classmethod
def _deserialize_data(cls, data: JSONdict, client: KGClient, include_id: bool = False):
# check types match
if cls.type_ not in data["@type"]:
if types_match(cls.type_, data["@type"][0]):
cls.type_ = data["@type"][0]
else:
raise TypeError("type mismatch {} - {}".format(cls.type_, data["@type"]))

# normalize data by expanding keys
if "om-i.org" in cls.type_:
cls.context = default_context["v4"]
else:
cls.context = default_context["v3"]

D = {"@type": data["@type"]}
if include_id:
D["@id"] = data["@id"]
Expand All @@ -340,8 +362,6 @@ def _deserialize_data(cls, data: JSONdict, client: KGClient, include_id: bool =
elif key[0] != "@":
normalised_key = expand_uri(key, cls.context)
D[normalised_key] = value
if cls.type_ not in D["@type"]:
raise TypeError("type mismatch {} - {}".format(cls.type_, D["@type"]))

def _get_type_from_data(data_item):
type_ = data_item.get("@type", None)
Expand Down
15 changes: 15 additions & 0 deletions fairgraph/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@

from .errors import AuthenticationError, AuthorizationError, ResourceExistsError
from .registry import lookup_type
from .queries import migrate_query

if TYPE_CHECKING:
from .kgobject import KGObject
Expand Down Expand Up @@ -132,6 +133,7 @@ def __init__(
self.cache: Dict[str, JsonLdDocument] = {}
self._query_cache: Dict[str, str] = {}
self.accepted_terms_of_use = False
self.migrated = None

@property
def _kg_admin_client(self):
Expand Down Expand Up @@ -194,6 +196,19 @@ def query(
A ResultPage object containing a list of JSON-LD instances that satisfy the query,
along with metadata about the query results such as total number of instances, and pagination information.
"""

# the following section is a temporary work-around for use during the transitional period
# from openMINDS v3 to v4 (change of namespace)
if self.migrated is None:
result = self.instance_from_full_uri("https://kg.ebrains.eu/api/instances/92631f2e-fc6e-4122-8015-a0731c67f66c", scope="released")
if "om-i.org" in result["@type"]:
self.migrated = True
else:
self.migrated = False

if self.migrated:
query = migrate_query(query)

query_id = query.get("@id", None)

if use_stored_query:
Expand Down
2 changes: 1 addition & 1 deletion fairgraph/kgobject.py
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,7 @@ def exists(

if instances:
if len(instances) > 1 and not ignore_duplicates:
raise Exception("Existence query is not specific enough")
raise Exception(f"Existence query is not specific enough. Type: {self.__class__.__name__}; filters: {query_filter}")

# it seems that sometimes the "query" endpoint returns instances
# which the "instances" endpoint doesn't know about, so here we double check that
Expand Down
7 changes: 0 additions & 7 deletions fairgraph/openminds/chemicals/amount_of_chemical.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,6 @@ class AmountOfChemical(EmbeddedMetadata):
"""

type_ = "https://openminds.ebrains.eu/chemicals/AmountOfChemical"
context = {
"schema": "http://schema.org/",
"kg": "https://kg.ebrains.eu/api/instances/",
"vocab": "https://openminds.ebrains.eu/vocab/",
"terms": "https://openminds.ebrains.eu/controlledTerms/",
"core": "https://openminds.ebrains.eu/core/",
}
properties = [
Property("amount", "openminds.core.QuantitativeValue", "vocab:amount", doc="no description available"),
Property(
Expand Down
Loading