From 6f13e99ce77361df87d4c9688de7b22d10a7f693 Mon Sep 17 00:00:00 2001 From: Andrew Davison Date: Wed, 27 May 2026 08:35:36 +0200 Subject: [PATCH] Remove openMINDS v3 transitional machinery now that the KG is on v4 Drops the KGClient.migrated feature-detect probe and all of the v3/v4 namespace adapters that were used to translate property, type and instance URIs. Also updates the queries.py docstring examples. --- doc/modules.rst | 2 +- fairgraph/client.py | 73 ++---------------------- fairgraph/node.py | 3 +- fairgraph/queries.py | 10 ++-- fairgraph/utility.py | 133 ------------------------------------------- test/test_client.py | 2 - test/test_queries.py | 9 --- test/test_utility.py | 46 --------------- 8 files changed, 12 insertions(+), 266 deletions(-) diff --git a/doc/modules.rst b/doc/modules.rst index 3425ae84..bd9cabf0 100644 --- a/doc/modules.rst +++ b/doc/modules.rst @@ -19,7 +19,7 @@ openMINDS modules/openminds_stimulation modules/openminds_publications -**fairgraph** currently provides the following modules for working with KG v3: +**fairgraph** currently provides the following modules: :doc:`modules/openminds_core` covers general origin, location and content of research products. diff --git a/fairgraph/client.py b/fairgraph/client.py index 59036d0c..941c38c5 100644 --- a/fairgraph/client.py +++ b/fairgraph/client.py @@ -19,7 +19,6 @@ # limitations under the License. from __future__ import annotations -from copy import deepcopy import os import logging from typing import Any, Dict, Iterable, List, Optional, Union, TYPE_CHECKING @@ -37,13 +36,7 @@ from openminds.registry import lookup_type from .errors import AuthenticationError, AuthorizationError, ResourceExistsError -from .utility import ( - adapt_namespaces_for_query, - adapt_namespaces_3to4, - adapt_namespaces_4to3, - adapt_type_4to3, - handle_scope_keyword, -) +from .utility import handle_scope_keyword from .base import OPENMINDS_VERSION if TYPE_CHECKING: @@ -144,7 +137,6 @@ def __init__( self.cache: Dict[str, JsonLdDocument] = {} self._query_cache: Dict[str, str] = {} self.accepted_terms_of_use = False - self._migrated = None if allow_interactive: self.user_info() @@ -214,30 +206,8 @@ def _check_response( else: raise Exception(f"Error: {response.error} {error_context}") else: - if self.migrated is False: - adapt_namespaces_3to4(response.data) return response - @property - def migrated(self): - # This is a temporary work-around for use during the transitional period - # from openMINDS v3 to v4 (change of namespace) - if self._migrated is None: - self._migrated = True # to stop the call to _check_response() in instance_from_full_uri from recurring - - # This is the released controlled term for "left handedness", which should be accessible to everyone - result = self.instance_from_full_uri( - "https://kg.ebrains.eu/api/instances/92631f2e-fc6e-4122-8015-a0731c67f66c", release_status="released" - ) - _type = result["@type"] - if isinstance(_type, list): - _type = _type[0] - if "om-i.org" in _type: - self._migrated = True - else: - self._migrated = False - return self._migrated - def query( self, query: Dict[str, Any], @@ -289,8 +259,6 @@ def _query(release_status, from_index, size): ) else: - if self.migrated is False: - query = adapt_namespaces_for_query(query) def _query(release_status, from_index, size): response = self._kg_client.queries.test_query( @@ -356,9 +324,6 @@ def list( """ release_status = handle_scope_keyword(scope, release_status) - if self.migrated is False: - target_type = adapt_type_4to3(target_type) - def _list(release_status, from_index, size): response = self._kg_client.instances.list( stage=STAGE_MAP[release_status], @@ -419,8 +384,7 @@ def _get_instance(release_status): error_context = f"_get_instance(release_status={release_status} uri={uri})" # Normal KG URIs start with https://kg.ebrains.eu/api/instances/ with a UUID # but for openMINDS controlled terms we may have the openMINDS URI - # of the form https://openminds.ebrains.eu/instances/ageCategory/juvenile (v3) - # or https://openminds.om-i.org/instances/ageCategory/juvenile (v4) + # of the form https://openminds.om-i.org/instances/ageCategory/juvenile # We use different query methods for these different cases. kg_namespace = self._kg_client.instances._kg_config.id_namespace if uri.startswith(kg_namespace): @@ -435,16 +399,8 @@ def _get_instance(release_status): data = None else: data = response.data - elif uri.startswith("https://openminds.om-i.org/instances") or uri.startswith( - "https://openminds.ebrains.eu/instances" - ): + elif uri.startswith("https://openminds.om-i.org/instances"): payload = [uri] - if self.migrated: - if uri.startswith("https://openminds.ebrains.eu"): - payload = [uri.replace("ebrains.eu", "om-i.org")] - else: - if uri.startswith("https://openminds.om-i.org"): - payload = [uri.replace("om-i.org", "ebrains.eu")] response = self._kg_client.instances.get_by_identifiers( stage=STAGE_MAP[release_status], payload=payload, @@ -491,9 +447,6 @@ def create_new_instance( raise ValueError("payload contains undefined ids") if instance_id: UUID(instance_id) - if self.migrated is False: - data = deepcopy(data) - adapt_namespaces_4to3(data) if instance_id: response = self._kg_client.instances.create_new_with_id( space=space, @@ -519,9 +472,6 @@ def update_instance(self, instance_id: str, data: JsonLdDocument) -> JsonLdDocum data (dict): a JSON-LD document that modifies some or all of the data of the existing instance. """ UUID(instance_id) - if self.migrated is False: - data = deepcopy(data) - adapt_namespaces_4to3(data) response = self._kg_client.instances.contribute_to_partial_replacement( instance_id=instance_id, payload=data, @@ -546,9 +496,6 @@ def replace_instance(self, instance_id: str, data: JsonLdDocument) -> JsonLdDocu data (dict): a JSON-LD document that will replace the existing instance. """ UUID(instance_id) - if self.migrated is False: - data = deepcopy(data) - adapt_namespaces_4to3(data) response = self._kg_client.instances.contribute_to_full_replacement( instance_id=instance_id, payload=data, @@ -746,10 +693,7 @@ def configure_space(self, space_name: Optional[str] = None, types: Optional[List ) raise Exception(err_msg) for cls in types: - if self.migrated: - target_type = cls.type_ - else: - target_type = adapt_type_4to3(cls.type_) + target_type = cls.type_ result = self._kg_admin_client.assign_type_to_space(space=space_name, target_type=target_type) if result: # error raise Exception(f"Unable to assign {cls.__name__} to space {space_name}: {result}") @@ -780,18 +724,12 @@ def space_info( number of instances of each class in the given spaces. """ release_status = handle_scope_keyword(scope, release_status) - # todo: if not self.migrated, adapt type before lookup result = self._kg_client.types.list(space=space_name, stage=STAGE_MAP[release_status]) if result.error: raise Exception(result.error) response = {} for item in result.data: - if self.migrated: - type_iri = item.identifier - else: - type_ = {"@type": item.identifier} - adapt_namespaces_3to4(type_) - type_iri = type_["@type"] + type_iri = item.identifier try: cls = lookup_type(type_iri, OPENMINDS_VERSION) except (KeyError, ValueError) as err: @@ -799,7 +737,6 @@ def space_info( "https://core.kg.ebrains.eu/vocab/type/Bookmark", "https://core.kg.ebrains.eu/vocab/meta/type/Query", "https://openminds.om-i.org/types/Query", - "https://openminds.ebrains.eu/core/URL", "https://openminds.om-i.org/types/URL" ] if ignore_errors or any(ignore in str(err) for ignore in ignore_list): diff --git a/fairgraph/node.py b/fairgraph/node.py index d039cd82..59f59d43 100644 --- a/fairgraph/node.py +++ b/fairgraph/node.py @@ -18,7 +18,6 @@ as_list, # temporary for backwards compatibility (a lot of code imports it from here) expand_uri, normalize_data, - types_match, ) if TYPE_CHECKING: @@ -308,7 +307,7 @@ def _normalize_type(data_item): type_from_data = _get_type_from_data(data) # check types match - if not types_match(cls.type_, type_from_data): + if cls.type_ != type_from_data: raise TypeError("type mismatch {} - {}".format(cls.type_, type_from_data)) # normalize data by expanding keys diff --git a/fairgraph/queries.py b/fairgraph/queries.py index 0da1a542..2c43e755 100644 --- a/fairgraph/queries.py +++ b/fairgraph/queries.py @@ -122,7 +122,7 @@ class QueryProperty: Example: >>> p = QueryProperty( - ... "https://openminds.ebrains.eu/vocab/fullName", + ... "https://openminds.om-i.org/props/fullName", ... name="full_name", ... filter=Filter("CONTAINS", parameter="name"), ... sorted=True, @@ -229,26 +229,26 @@ class Query: Example: >>> q = Query( - ... node_type="https://openminds.ebrains.eu/core/ModelVersion", + ... node_type="https://openminds.om-i.org/types/ModelVersion", ... label="fg-testing-modelversion", ... space="model", ... properties=[ ... QueryProperty("@type"), ... QueryProperty( - ... "https://openminds.ebrains.eu/vocab/fullName", + ... "https://openminds.om-i.org/props/fullName", ... name="vocab:fullName", ... filter=Filter("CONTAINS", parameter="name"), ... sorted=True, ... required=True, ... ), ... QueryProperty( - ... "https://openminds.ebrains.eu/vocab/versionIdentifier", + ... "https://openminds.om-i.org/props/versionIdentifier", ... name="vocab:versionIdentifier", ... filter=Filter("EQUALS", parameter="version"), ... required=True, ... ), ... QueryProperty( - ... "https://openminds.ebrains.eu/vocab/format", + ... "https://openminds.om-i.org/props/format", ... name="vocab:format", ... ensure_order=True, ... properties=[ diff --git a/fairgraph/utility.py b/fairgraph/utility.py index d9e61f8e..ada6510f 100644 --- a/fairgraph/utility.py +++ b/fairgraph/utility.py @@ -18,16 +18,11 @@ # limitations under the License. from __future__ import annotations -from copy import deepcopy import hashlib import logging from typing import Any, Dict, Iterable, List, Optional, Tuple, Union, TYPE_CHECKING import warnings -from openminds.registry import lookup_type - -from .base import OPENMINDS_VERSION - if TYPE_CHECKING: from .client import KGClient from .kgobject import KGObject @@ -446,134 +441,6 @@ def accepted_terms_of_use(client: KGClient, accept_terms_of_use: bool = False) - return False -def types_match(a, b): - # temporarily, during the openMINDS transition v3-v4, we allow different namespaces for the types - assert isinstance(a, str), a - assert isinstance(b, str), b - if a == b: - return True - elif a.split("/")[-1] == b.split("/")[-1]: - logger.warning(f"Assuming {a} matches {b} in types_match()") - return True - else: - return False - - -def _adapt_namespaces(data, adapt_keys, adapt_type, adapt_instance_uri): - if isinstance(data, list): - for item in data: - _adapt_namespaces(item, adapt_keys, adapt_type, adapt_instance_uri) - elif isinstance(data, dict): - # adapt property URIs - old_keys = tuple(data.keys()) - new_keys = adapt_keys(old_keys) - for old_key, new_key in zip(old_keys, new_keys): - data[new_key] = data.pop(old_key) - for key, value in data.items(): - if key == "@id": - data[key] = adapt_instance_uri(value) - elif isinstance(value, (list, dict)): - _adapt_namespaces(value, adapt_keys, adapt_type, adapt_instance_uri) - # adapt @type URIs - if "@type" in data: - data["@type"] = adapt_type(data["@type"]) - else: - pass - - -def adapt_namespaces_3to4(data): - - def adapt_keys_3to4(uri_list): - replacement = ("openminds.ebrains.eu/vocab", "openminds.om-i.org/props") - return (uri.replace(*replacement) for uri in uri_list) - - def adapt_type_3to4(uri): - if isinstance(uri, list): - assert len(uri) == 1 - uri = uri[0] - return f"https://openminds.om-i.org/types/{uri.split('/')[-1]}" - - def adapt_instance_uri_3to4(uri): - if uri.startswith("https://openminds"): - return uri.replace("ebrains.eu", "om-i.org") - else: - return uri - - return _adapt_namespaces(data, adapt_keys_3to4, adapt_type_3to4, adapt_instance_uri_3to4) - - -def adapt_type_4to3(uri): - if isinstance(uri, list): - assert len(uri) == 1 - uri = uri[0] - cls = lookup_type(uri, OPENMINDS_VERSION) - - if cls.__module__ == "test.test_client": - return cls.type_ - - module_name = cls.__module__.split(".")[2] # e.g., 'fairgraph.openminds.core.actors.person' -> "core" - module_name = {"controlled_terms": "controlledTerms", "specimen_prep": "specimenPrep"}.get( - module_name, module_name - ) - return f"https://openminds.ebrains.eu/{module_name}/{cls.__name__}" - - -def adapt_namespaces_4to3(data): - - def adapt_keys_4to3(uri_list): - replacement = ("openminds.om-i.org/props", "openminds.ebrains.eu/vocab") - return (uri.replace(*replacement) for uri in uri_list) - - def adapt_instance_uri_4to3(uri): - if uri.startswith("https://openminds"): - return uri.replace("om-i.org", "ebrains.eu") - else: - return uri - - return _adapt_namespaces(data, adapt_keys_4to3, adapt_type_4to3, adapt_instance_uri_4to3) - - -def adapt_namespaces_for_query(query): - """Map from v4+ to v3 openMINDS namespace""" - - def adapt_path(item_path, replacement): - if isinstance(item_path, str): - return item_path.replace(*replacement) - elif isinstance(item_path, list): - return [adapt_path(part, replacement) for part in item_path] - else: - assert isinstance(item_path, dict) - new_item_path = item_path.copy() - new_item_path["@id"] = item_path["@id"].replace(*replacement) - if "typeFilter" in item_path: - if isinstance(item_path["typeFilter"], list): - new_item_path["typeFilter"] = [ - {"@id": adapt_type_4to3(subitem["@id"])} for subitem in item_path["typeFilter"] - ] - else: - new_item_path["typeFilter"]["@id"] = adapt_type_4to3(item_path["typeFilter"]["@id"]) - return new_item_path - - def adapt_structure(structure, replacement): - for item in structure: - item["path"] = adapt_path(item["path"], replacement) - if "structure" in item: - adapt_structure(item["structure"], replacement) - - def adapt_filters(structure, replacement): - for item in structure: - if "filter" in item and "value" in item["filter"]: - item["filter"]["value"] = item["filter"]["value"].replace(*replacement) - if "structure" in item: - adapt_filters(item["structure"], replacement) - - migrated_query = deepcopy(query) - migrated_query["meta"]["type"] = adapt_type_4to3(migrated_query["meta"]["type"]) - adapt_structure(migrated_query["structure"], ("openminds.om-i.org/props", "openminds.ebrains.eu/vocab")) - adapt_filters(migrated_query["structure"], ("openminds.om-i.org/instances", "openminds.ebrains.eu/instances")) - return migrated_query - - def initialise_instances(class_list): """Cast openMINDS instances to their fairgraph subclass""" for cls in class_list: diff --git a/test/test_client.py b/test/test_client.py index c107a45d..405594dd 100644 --- a/test/test_client.py +++ b/test/test_client.py @@ -244,8 +244,6 @@ def offline_kg_client(mocker): from fairgraph.client import KGClient client = KGClient(token="fake-token", allow_interactive=False) - # Skip the feature-detection fetch that the `migrated` property triggers. - client._migrated = True # `instance_from_full_uri` uses this to build the cache key after writes mocker.patch.object( client._kg_client.instances._kg_config, diff --git a/test/test_queries.py b/test/test_queries.py index 2990b0aa..1d2d4674 100644 --- a/test/test_queries.py +++ b/test/test_queries.py @@ -3,7 +3,6 @@ import pytest from kg_core.request import Stage, Pagination from fairgraph.queries import Query, QueryProperty, Filter, PathElement -from fairgraph.utility import adapt_namespaces_for_query import fairgraph.openminds.core as omcore from .utils import kg_client, mock_client, skip_if_no_connection @@ -363,8 +362,6 @@ def test_query_with_reverse_properties(example_query_repository_with_reverse): @skip_if_no_connection def test_execute_query(kg_client, example_query_model_version): query = example_query_model_version.serialize() - if kg_client.migrated is False: - query = adapt_namespaces_for_query(query) response = kg_client._kg_client.queries.test_query( payload=query, stage=Stage.RELEASED, @@ -398,8 +395,6 @@ def test_execute_query(kg_client, example_query_model_version): def test_execute_query_with_id_filter(kg_client, example_query_model): target_id = "https://kg.ebrains.eu/api/instances/3ca9ae35-c9df-451f-ac76-4925bd2c7dc6" query = example_query_model.serialize() - if kg_client.migrated is False: - query = adapt_namespaces_for_query(query) response = kg_client._kg_client.queries.test_query( payload=query, instance_id=kg_client.uuid_from_uri(target_id), @@ -417,8 +412,6 @@ def test_execute_query_with_id_filter(kg_client, example_query_model): def test_execute_query_with_reverse_properties_and_instance_id(kg_client, example_query_repository_with_reverse): target_id = "https://kg.ebrains.eu/api/instances/1c846a5f-eac2-477a-9dc3-d2e51b00fda9" query = example_query_repository_with_reverse.serialize() - if kg_client.migrated is False: - query = adapt_namespaces_for_query(query) response = kg_client._kg_client.queries.test_query( payload=query, instance_id=kg_client.uuid_from_uri(target_id), @@ -438,8 +431,6 @@ def test_execute_query_with_reverse_properties_and_instance_id(kg_client, exampl @skip_if_no_connection def test_execute_query_with_reverse_properties_and_filter(kg_client, example_query_repository_with_reverse): query = example_query_repository_with_reverse.serialize() - if kg_client.migrated is False: - query = adapt_namespaces_for_query(query) response = kg_client._kg_client.queries.test_query( payload=query, additional_request_params={ diff --git a/test/test_utility.py b/test/test_utility.py index 3d78e288..465614b3 100644 --- a/test/test_utility.py +++ b/test/test_utility.py @@ -1,4 +1,3 @@ -from copy import deepcopy import os import tempfile import pytest @@ -9,8 +8,6 @@ accepted_terms_of_use, sha1sum, normalize_data, - adapt_namespaces_3to4, - adapt_namespaces_4to3, ) from .utils import kg_client, skip_if_no_connection @@ -102,46 +99,3 @@ def test_normalize_data(): assert normalize_data(data, context) == expected -def test_adapt_namespaces(): - import fairgraph.openminds.core # needed to populate the registry for lookup - - data_v3 = [ - { - "@id": "0000", - "@type": "https://openminds.ebrains.eu/core/Person", - "https://openminds.ebrains.eu/vocab/affiliation": { - "@type": "https://openminds.ebrains.eu/core/Affiliation", - "https://openminds.ebrains.eu/vocab/memberOf": { - "@type": "https://openminds.ebrains.eu/core/Organization", - "https://openminds.ebrains.eu/vocab/fullName": "The Lonely Mountain", - }, - }, - "https://openminds.ebrains.eu/vocab/familyName": "Oakenshield", - "https://openminds.ebrains.eu/vocab/givenName": "Thorin", - } - ] - data_v4 = [ - { - "@id": "0000", - "@type": "https://openminds.om-i.org/types/Person", - "https://openminds.om-i.org/props/affiliation": { - "@type": "https://openminds.om-i.org/types/Affiliation", - "https://openminds.om-i.org/props/memberOf": { - "@type": "https://openminds.om-i.org/types/Organization", - "https://openminds.om-i.org/props/fullName": "The Lonely Mountain", - }, - }, - "https://openminds.om-i.org/props/familyName": "Oakenshield", - "https://openminds.om-i.org/props/givenName": "Thorin", - } - ] - - data = deepcopy(data_v3) - adapt_namespaces_3to4(data) - assert data == data_v4 - - data = deepcopy(data_v4) - adapt_namespaces_4to3(data) - assert data == data_v3 - - assert data_v3 != data_v4