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

Added method to get context from model or store and another to expand the url using a context #315

Merged
merged 16 commits into from Jul 17, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -12,7 +12,7 @@
}
encodingFormat: x._mediaType
name: x._filename
contentUrl: x._self
contentUrl: forge.expand_uri(x._self)
atLocation:
{
type: Location
Expand Down
16 changes: 15 additions & 1 deletion kgforge/core/archetypes/store.py
Expand Up @@ -21,6 +21,7 @@

from kgforge.core import Resource
from kgforge.core.commons.attributes import repr_class
from kgforge.specializations.mappers import DictionaryMapper
from kgforge.core.commons.context import Context
from kgforge.core.commons.exceptions import (
DeprecationError,
Expand Down Expand Up @@ -136,6 +137,7 @@ def __init__(
if hasattr(self.service, "metadata_context")
else None
)
self._mapper = None

def __repr__(self) -> str:
return repr_class(self)
Expand All @@ -149,6 +151,10 @@ def mapping(self) -> Optional[Callable]:
def mapper(self) -> Optional[Callable]:
"""Mapper class to map file metadata to a Resource with file_resource_mapping."""
return None

@mapper.setter
def mapper(self, mapper: Optional[DictionaryMapper]) -> Optional[DictionaryMapper]:
self._mapper = mapper

# [C]RUD.

Expand Down Expand Up @@ -185,7 +191,7 @@ def upload(self, path: str, content_type: str) -> Union[Resource, List[Resource]
if self.file_mapping is not None:
p = Path(path)
uploaded = self._upload(p, content_type)
return self.mapper().map(uploaded, self.file_mapping, None)
return self.mapper.map(uploaded, self.file_mapping, None)
else:
raise UploadingError("no file_resource_mapping has been configured")

Expand Down Expand Up @@ -510,6 +516,14 @@ def _debug_query(query):
else:
print(*["Submitted query:", *query.splitlines()], sep="\n ")
print()

def expand_uri(self, uri: str, context: Context, is_file: bool, encoding: str) -> str:
"""Expand a given url using the store or model context

:param uri: 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):
Expand Down
30 changes: 25 additions & 5 deletions kgforge/core/forge.py
Expand Up @@ -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
Expand Down Expand Up @@ -215,12 +218,12 @@ 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")
self._store: Store = store(**store_config)
store_config.update(name=store_name)
self._store.mapper = DictionaryMapper(self)
crisely09 marked this conversation as resolved.
Show resolved Hide resolved

# Resolvers.
resolvers_config = config.pop("Resolvers", None)
Expand All @@ -234,8 +237,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]]:
"""
Expand Down Expand Up @@ -744,7 +745,6 @@ def as_json(
self._model.resolve_context,
)

@catch
@catch
def as_jsonld(
self,
Expand Down Expand Up @@ -880,7 +880,27 @@ def from_dataframe(
:return: Union[Resource, List[Resource]]
"""
return from_dataframe(data, na, nesting)


def get_store_context(self):
"""Expose the context used in the store."""
return self._store.context

def get_model_context(self):
"""Expose the context used in the model."""
return self._model.context()

def expand_uri(self, uri: 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 uri: the uri 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_store_context()
return self._store.expand_uri(uri, context, is_file, encoding)

def prepare_resolvers(
config: Dict, store_config: Dict
Expand Down
37 changes: 34 additions & 3 deletions kgforge/specializations/stores/bluebrain_nexus.py
Expand Up @@ -135,9 +135,13 @@ def mapping(self) -> Optional[Callable]:
return DictionaryMapping

@property
def mapper(self) -> Optional[Callable]:
return DictionaryMapper

def mapper(self) -> Optional[DictionaryMapper]:
return self._mapper

@mapper.setter
def mapper(self, mapper: Optional[DictionaryMapper]) -> Optional[DictionaryMapper]:
self._mapper = mapper

def register(
self, data: Union[Resource, List[Resource]], schema_id: str = None
) -> None:
Expand Down Expand Up @@ -1019,6 +1023,33 @@ def _initialize_service(
files_download_config=files_download_config,
**params,
)

def expand_uri(self, uri: str, context: Context, is_file, encoding):
# try decoding the url first
raw_url = unquote(uri)
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.expand(groups[0])
print(groups[0], resolved)
if raw_url.startswith(url_base):
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:
Expand Down
3 changes: 3 additions & 0 deletions kgforge/specializations/stores/demo_store.py
Expand Up @@ -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_uri(self, uri: str, context: Context, is_file: bool, encoding: str) -> str:
raise not_supported()

class RecordExists(Exception):
pass
Expand Down
4 changes: 2 additions & 2 deletions kgforge/specializations/stores/nexus/service.py
Expand Up @@ -95,7 +95,6 @@ def __init__(
files_download_config: Dict,
**params,
):

nexus.config.set_environment(endpoint)
self.endpoint = endpoint
self.organisation = org
Expand Down Expand Up @@ -241,7 +240,8 @@ 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"]}
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:
Expand Down
37 changes: 33 additions & 4 deletions tests/specializations/stores/test_bluebrain_nexus.py
Expand Up @@ -18,18 +18,19 @@
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
from kgforge.core.commons.context import Context
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,
Expand All @@ -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")
Expand Down Expand Up @@ -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="")
Expand Down Expand Up @@ -165,6 +171,29 @@ 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_uri(url, context=nexus_context, is_file=True, encoding=None)
assert expected == uri


class TestQuerying:
@pytest.fixture
def context(self):
Expand Down