diff --git a/examples/configurations/nexus-store/file-to-resource-mapping.hjson b/examples/configurations/nexus-store/file-to-resource-mapping.hjson index d9fc787c..e12554dc 100644 --- a/examples/configurations/nexus-store/file-to-resource-mapping.hjson +++ b/examples/configurations/nexus-store/file-to-resource-mapping.hjson @@ -12,7 +12,7 @@ } encodingFormat: x._mediaType name: x._filename - contentUrl: x._self + contentUrl: forge.expand_url(x.id) atLocation: { type: Location diff --git a/kgforge/core/archetypes/store.py b/kgforge/core/archetypes/store.py index 40a6504b..744d8ae9 100644 --- a/kgforge/core/archetypes/store.py +++ b/kgforge/core/archetypes/store.py @@ -510,6 +510,14 @@ def _debug_query(query): else: print(*["Submitted query:", *query.splitlines()], sep="\n ") print() + + def expand_url(self, url: str, context: Context, is_file: bool, encoding: str) -> str: + """Expand a given url using the store or model context + + :param url: the idenfitier to be transformed + :param context: a Context object with vocabulary to be used in the construction of the URI + """ + pass def _replace_in_sparql(qr, what, value, default_value, search_regex, replace_if_in_query=True): diff --git a/kgforge/core/forge.py b/kgforge/core/forge.py index 83e27ef3..06840f79 100644 --- a/kgforge/core/forge.py +++ b/kgforge/core/forge.py @@ -16,14 +16,17 @@ from pathlib import Path from typing import Any, Callable, Dict, List, Optional, Tuple, Union +import re import numpy as np import yaml from kgforge.core.commons.files import load_file_as_byte from pandas import DataFrame from rdflib import Graph +from urllib.parse import quote_plus, urlparse from kgforge.core import Resource from kgforge.core.archetypes import Mapping, Model, Resolver, Store +from kgforge.core.commons.context import Context from kgforge.core.commons.actions import LazyAction from kgforge.core.commons.dictionaries import with_defaults from kgforge.core.commons.exceptions import ResolvingError @@ -215,7 +218,6 @@ def __init__(self, configuration: Union[str, Dict], **kwargs) -> None: self._model: Model = model(**model_config) # Store. - store_config.update(model_context=self._model.context()) store_name = store_config.pop("name") store = import_class(store_name, "stores") @@ -234,8 +236,6 @@ def __init__(self, configuration: Union[str, Dict], **kwargs) -> None: # Formatters. self._formatters: Optional[Dict[str, str]] = config.pop("Formatters", None) - # Modeling User Interface. - @catch def prefixes(self, pretty: bool = True) -> Optional[Dict[str, str]]: """ @@ -744,7 +744,6 @@ def as_json( self._model.resolve_context, ) - @catch @catch def as_jsonld( self, @@ -880,7 +879,26 @@ def from_dataframe( :return: Union[Resource, List[Resource]] """ return from_dataframe(data, na, nesting) - + + def get_context(self, origin : str ="model"): + """Expose the context used in the model or in the store.""" + if origin == "model": + return self._model.context() + elif origin == "store": + return self._store.context + + def expand_url(self, url: str, context: Context = None, + is_file: bool = True, encoding: str = None): + """ + Construct an URI a given an id using the vocabulary given in the Context object. + + :param url: the url to transform + :param context: a Context object that should be used to create the URI + :param encode: parameter to use to encode or not the uri, default is `utf-8` + """ + if context is None: + context = self.get_context("store") + return self._store.expand_url(url, context, is_file, encoding) def prepare_resolvers( config: Dict, store_config: Dict diff --git a/kgforge/specializations/stores/bluebrain_nexus.py b/kgforge/specializations/stores/bluebrain_nexus.py index 9b0913ae..88762bbe 100644 --- a/kgforge/specializations/stores/bluebrain_nexus.py +++ b/kgforge/specializations/stores/bluebrain_nexus.py @@ -1019,6 +1019,33 @@ def _initialize_service( files_download_config=files_download_config, **params, ) + + def expand_url(self, url: str, context: Context, is_file, encoding): + # try decoding the url first + raw_url = unquote(url) + if is_file: # for files + url_base = '/'.join([self.endpoint, 'files', self.bucket]) + else: # for resources + url_base = '/'.join([self.endpoint, 'resources', self.bucket]) + matches = re.match(r"[\w\.:%/-]+/(\w+):(\w+)/[\w\.-/:%]+", raw_url) + if matches: + groups = matches.groups() + old_schema = f"{groups[0]}:{groups[1]}" + resolved = context.resolve(groups[0]) + if raw_url.startswith(url_base): + print('I am here', resolved) + extended_schema = '/'.join([quote_plus(resolved), groups[1]]) + url = raw_url.replace(old_schema, extended_schema) + return url + else: + extended_schema = '/'.join([resolved, groups[1]]) + url = raw_url.replace(old_schema, extended_schema) + else: + url = raw_url + if url.startswith(url_base): + return url + uri = "/".join((url_base, quote_plus(url, encoding=encoding))) + return uri def _error_message(error: HTTPError) -> str: diff --git a/kgforge/specializations/stores/demo_store.py b/kgforge/specializations/stores/demo_store.py index cce34d2d..81ef87f2 100644 --- a/kgforge/specializations/stores/demo_store.py +++ b/kgforge/specializations/stores/demo_store.py @@ -238,6 +238,9 @@ def _archive_id(rid: str, version: int) -> str: @staticmethod def _tag_id(rid: str, tag: str) -> str: return f"{rid}_tag={tag}" + + def expand_url(self, url: str, context: Context, is_file: bool, encoding: str) -> str: + return url class RecordExists(Exception): pass diff --git a/kgforge/specializations/stores/nexus/service.py b/kgforge/specializations/stores/nexus/service.py index ebcf3edf..30af81ae 100644 --- a/kgforge/specializations/stores/nexus/service.py +++ b/kgforge/specializations/stores/nexus/service.py @@ -95,7 +95,6 @@ def __init__( files_download_config: Dict, **params, ): - nexus.config.set_environment(endpoint) self.endpoint = endpoint self.organisation = org @@ -241,7 +240,9 @@ def __init__( def get_project_context(self) -> Dict: project_data = nexus.projects.fetch(self.organisation, self.project) - context = {"@base": project_data["base"], "@vocab": project_data["vocab"]} + print('api mappings', project_data['apiMappings']) + context = {"@base": project_data["base"], "@vocab": project_data["vocab"], + "api_mappings": project_data["apiMappings"]} return context def resolve_context(self, iri: str, local_only: Optional[bool] = False) -> Dict: diff --git a/tests/specializations/stores/test_bluebrain_nexus.py b/tests/specializations/stores/test_bluebrain_nexus.py index 343b1992..e318a897 100644 --- a/tests/specializations/stores/test_bluebrain_nexus.py +++ b/tests/specializations/stores/test_bluebrain_nexus.py @@ -18,11 +18,11 @@ from urllib.parse import urljoin from urllib.request import pathname2url from uuid import uuid4 -from kgforge.core.commons.sparql_query_builder import SPARQLQueryBuilder import nexussdk import pytest from typing import Callable, Union, List +from collections import OrderedDict from kgforge.core import Resource from kgforge.core.archetypes import Store @@ -30,6 +30,7 @@ from kgforge.core.conversions.rdf import _merge_jsonld from kgforge.core.wrappings.dict import wrap_dict from kgforge.core.wrappings.paths import Filter, create_filters_from_dict +from kgforge.core.commons.sparql_query_builder import SPARQLQueryBuilder from kgforge.specializations.stores.bluebrain_nexus import ( BlueBrainNexus, _create_select_query, @@ -40,10 +41,10 @@ from kgforge.specializations.stores.nexus import Service BUCKET = "test/kgforge" -NEXUS = "https://nexus-instance.org/" +NEXUS = "https://nexus-instance.org" TOKEN = "token" -NEXUS_PROJECT_CONTEXT = {"base": "http://data.net/", "vocab": "http://vocab.net/"} - +NEXUS_PROJECT_CONTEXT = {"base": "http://data.net", "vocab": "http://vocab.net", + "apiMappings": [{'namespace': 'https://neuroshapes.org/dash/', 'prefix': 'datashapes'}]} VERSIONED_TEMPLATE = "{x.id}?rev={x._store_metadata._rev}" FILE_RESOURCE_MAPPING = os.sep.join( (os.path.curdir, "tests", "data", "nexus-store", "file-to-resource-mapping.hjson") @@ -130,6 +131,11 @@ def nexus_store_unauthorized(): return BlueBrainNexus(endpoint=NEXUS, bucket=BUCKET, token="invalid token") +@pytest.fixture +def nexus_context(): + return Context(NEXUS_PROJECT_CONTEXT) + + def test_config_error(): with pytest.raises(ValueError): BlueBrainNexus(endpoint="test", bucket="invalid", token="") @@ -165,6 +171,30 @@ def test_to_resource(nexus_store, registered_building, building_jsonld): assert str(result._store_metadata) == str(registered_building._store_metadata) +@pytest.mark.parametrize("url,expected", + [ + pytest.param( + ("myverycoolid123456789"), + ("https://nexus-instance.org/files/test/kgforge/myverycoolid123456789"), + id="simple-id", + ), + pytest.param( + ("https://nexus-instance.org/files/test/kgforge/myverycoolid123456789"), + ("https://nexus-instance.org/files/test/kgforge/myverycoolid123456789"), + id="same-id", + ), + # pytest.param( + # ("https://nexus-instance.org/files/test/kgforge/datashapes:example/myverycoolid123456789"), + # ("https://nexus-instance.org/files/test/kgforge/https%3A%2F%2Fbbp.epfl.ch%2Fneurosciencegraph%2Fdata%2Fdatashapes/example/myverycoolid123456789"), + # id="schema-id", + # ), + ]) +def test_expand_url(nexus_store, nexus_context, url, expected): + uri = nexus_store.expand_url(url, context=nexus_context, is_file=True, encoding=None) + print(uri) + assert expected == uri + + class TestQuerying: @pytest.fixture def context(self):