Bläddra i källkod

Allow creating resource with RDF string OR rdflib.Graph objects

* Also add resource API tests.
Stefano Cossu 6 år sedan
förälder
incheckning
f9b4508e5e
4 ändrade filer med 215 tillägg och 54 borttagningar
  1. 8 6
      lakesuperior/api/resource.py
  2. 46 37
      lakesuperior/model/ldp_factory.py
  3. 23 11
      lakesuperior/model/ldpr.py
  4. 138 0
      tests/test_resource_api.py

+ 8 - 6
lakesuperior/api/resource.py

@@ -77,7 +77,7 @@ def transaction(write=False):
             with TxnManager(env.app_globals.rdf_store, write=write) as txn:
                 ret = fn(*args, **kwargs)
             if len(env.app_globals.changelog):
-                job = Thread(target=process_queue)
+                job = Thread(target=_process_queue)
                 job.start()
             delattr(env, 'timestamp')
             delattr(env, 'timestamp_term')
@@ -86,18 +86,18 @@ def transaction(write=False):
     return _transaction_deco
 
 
-def process_queue():
+def _process_queue():
     """
     Process the message queue on a separate thread.
     """
     lock = Lock()
     lock.acquire()
     while len(env.app_globals.changelog):
-        send_event_msg(*env.app_globals.changelog.popleft())
+        _send_event_msg(*env.app_globals.changelog.popleft())
     lock.release()
 
 
-def send_event_msg(remove_trp, add_trp, metadata):
+def _send_event_msg(remove_trp, add_trp, metadata):
     """
     Send messages about a changed LDPR.
 
@@ -199,7 +199,8 @@ def create(parent, slug, **kwargs):
     :param str parent: UID of the parent resource.
     :param str slug: Tentative path relative to the parent UID.
     :param \*\*kwargs: Other parameters are passed to the
-      :meth:`LdpFactory.from_provided` method.
+      :py:meth:`~lakesuperior.model.ldp_factory.LdpFactory.from_provided`
+      method.
 
     :rtype: str
     :return: UID of the new resource.
@@ -227,7 +228,8 @@ def create_or_replace(uid, stream=None, **kwargs):
     :param BytesIO stream: Content stream. If empty, an empty container is
         created.
     :param \*\*kwargs: Other parameters are passed to the
-        :meth:`LdpFactory.from_provided` method.
+        :py:meth:`~lakesuperior.model.ldp_factory.LdpFactory.from_provided`
+        method.
 
     :rtype: str
     :return: Event type: whether the resource was created or updated.

+ 46 - 37
lakesuperior/model/ldp_factory.py

@@ -79,9 +79,12 @@ class LdpFactory:
 
     @staticmethod
     def from_provided(
-            uid, mimetype=None, stream=None, provided_imr=None, **kwargs):
+            uid, mimetype=None, stream=None, init_gr=None, **kwargs):
         r"""
-        Determine LDP type from request content.
+        Create and LDPR instance from provided data.
+
+        the LDP class (LDP-RS, LDP_NR, etc.) is determined by the contents
+        passed.
 
         :param str uid: UID of the resource to be created or updated.
         :param str mimetype: The provided content MIME type.
@@ -89,54 +92,60 @@ class LdpFactory:
             RDF or non-RDF content, or None. In the latter case, an empty
             container is created.
         :type stream: IOStream or None
+        :param rdflib.Graph init_gr: Initial graph to populate the
+            resource with, alternatively to ``stream``. This can be
+            used for LDP-RS and LDP-NR types alike.
         :param \*\*kwargs: Arguments passed to the LDP class constructor.
         """
         uri = nsc['fcres'][uid]
 
-        if not stream and not mimetype:
-            # Create empty LDPC.
+        # If no content or MIME type is passed, create an empty LDPC.
+        if not any((stream, mimetype, init_gr)):
             logger.info('No data received in request. '
                     'Creating empty container.')
             inst = Ldpc(uid, provided_imr=Graph(identifier=uri), **kwargs)
-        elif __class__.is_rdf_parsable(mimetype):
-            # Create container and populate it with provided RDF data.
-            input_rdf = stream.read()
-            imr = Graph(identifier=uri).parse(
-                    data=input_rdf, format=mimetype, publicID=uri)
+
+        else:
+            # If the stream is RDF, or an IMR is provided, create a container
+            # and populate it with provided RDF data.
+            provided_imr = Graph(identifier=uri)
+            # Provided RDF stream overrides provided IMR.
+            if __class__.is_rdf_parsable(mimetype) :
+                provided_imr.parse(
+                        data=stream.read(), format=mimetype, publicID=uri)
+            elif init_gr:
+                provided_imr += init_gr
             #logger.debug('Provided graph: {}'.format(
-            #        pformat(set(provided_gr))))
-            provided_imr = imr
-
-            # Determine whether it is a basic, direct or indirect container.
-            if Ldpr.MBR_RSRC_URI in imr.predicates() and \
-                    Ldpr.MBR_REL_URI in imr.predicates():
-                if Ldpr.INS_CNT_REL_URI in imr.predicates():
-                    cls = LdpIc
+            #        pformat(set(provided_imr))))
+
+            if not mimetype or __class__.is_rdf_parsable(mimetype):
+                # Determine whether it is a basic, direct or indirect container.
+                if Ldpr.MBR_RSRC_URI in provided_imr.predicates() and \
+                        Ldpr.MBR_REL_URI in provided_imr.predicates():
+                    if Ldpr.INS_CNT_REL_URI in provided_imr.predicates():
+                        cls = LdpIc
+                    else:
+                        cls = LdpDc
                 else:
-                    cls = LdpDc
-            else:
-                cls = Ldpc
+                    cls = Ldpc
 
-            inst = cls(uid, provided_imr=provided_imr, **kwargs)
+                inst = cls(uid, provided_imr=provided_imr, **kwargs)
 
-            # Make sure we are not updating an LDP-RS with an LDP-NR.
-            if inst.is_stored and LDP_NR_TYPE in inst.ldp_types:
-                raise IncompatibleLdpTypeError(uid, mimetype)
+                # Make sure we are not updating an LDP-RS with an LDP-NR.
+                if inst.is_stored and LDP_NR_TYPE in inst.ldp_types:
+                    raise IncompatibleLdpTypeError(uid, mimetype)
 
-            if kwargs.get('handling', 'strict') != 'none':
-                inst._check_mgd_terms(inst.provided_imr)
+                if kwargs.get('handling', 'strict') != 'none':
+                    inst._check_mgd_terms(inst.provided_imr)
 
-        else:
-            # Create a LDP-NR and equip it with the binary file provided.
-            # The IMR can also be provided for additional metadata.
-            if not provided_imr:
-                provided_imr = Graph(identifier=uri)
-            inst = LdpNr(uid, stream=stream, mimetype=mimetype,
-                    provided_imr=provided_imr, **kwargs)
-
-            # Make sure we are not updating an LDP-NR with an LDP-RS.
-            if inst.is_stored and LDP_RS_TYPE in inst.ldp_types:
-                raise IncompatibleLdpTypeError(uid, mimetype)
+            else:
+                # Create a LDP-NR and equip it with the binary file provided.
+                inst = LdpNr(uid, stream=stream, mimetype=mimetype,
+                        provided_imr=provided_imr, **kwargs)
+
+                # Make sure we are not updating an LDP-NR with an LDP-RS.
+                if inst.is_stored and LDP_RS_TYPE in inst.ldp_types:
+                    raise IncompatibleLdpTypeError(uid, mimetype)
 
         logger.info('Creating resource of type: {}'.format(
                 inst.__class__.__name__))

+ 23 - 11
lakesuperior/model/ldpr.py

@@ -27,15 +27,13 @@ logger = logging.getLogger(__name__)
 
 
 class Ldpr(metaclass=ABCMeta):
-    """LDPR (LDP Resource).
-
-    Definition: https://www.w3.org/TR/ldp/#ldpr-resource
+    """
+    LDPR (LDP Resource).
 
     This class and related subclasses contain the implementation pieces of
-    the vanilla LDP specifications. This is extended by the
-    `lakesuperior.fcrepo.Resource` class.
-
-    See inheritance graph: https://www.w3.org/TR/ldp/#fig-ldpc-types
+    the `LDP Resource <https://www.w3.org/TR/ldp/#ldpr-resource>`__
+    specifications, according to their `inheritance graph
+    <https://www.w3.org/TR/ldp/#fig-ldpc-types>`__.
 
     **Note**: Even though LdpNr (which is a subclass of Ldpr) handles binary
     files, it still has an RDF representation in the triplestore. Hence, some
@@ -144,12 +142,26 @@ class Ldpr(metaclass=ABCMeta):
     @property
     def imr(self):
         """
-        Extract an in-memory resource from the graph store.
+        In-Memory Resource.
 
-        If the resource is not stored (yet), a `ResourceNotExistsError` is
-        raised.
+        This is a copy of the resource extracted from the graph store. It is a
+        graph resource whose identifier is the URI of the resource.
 
-        :rtype: rdflib.Resource
+        >>> rsrc = rsrc_api.get('/')
+        >>> rsrc.imr.identifier
+        rdflib.term.URIRef('info:fcres/')
+        >>> rsrc.imr.value(rsrc.imr.identifier, nsc['fcrepo'].lastModified)
+        rdflib.term.Literal(
+            '2018-04-03T05:20:33.774746+00:00',
+            datatype=rdflib.term.URIRef(
+                'http://www.w3.org/2001/XMLSchema#dateTime'))
+
+        The IMR can be read and manipulated, as well as used to
+        update the stored resource.
+
+        :rtype: rdflib.Graph
+        :raise lakesuperior.exceptions.ResourceNotExistsError: If the resource
+            is not stored (yet).
         """
         if not hasattr(self, '_imr'):
             if hasattr(self, '_imr_options'):

+ 138 - 0
tests/test_resource_api.py

@@ -0,0 +1,138 @@
+import pdb
+import pytest
+
+from io import BytesIO
+from uuid import uuid4
+
+from rdflib import Graph, Literal, URIRef
+
+from lakesuperior.api import resource as rsrc_api
+from lakesuperior.dictionaries.namespaces import ns_collection as nsc
+from lakesuperior.exceptions import (
+        InvalidResourceError, ResourceNotExistsError, TombstoneError)
+from lakesuperior.globals import RES_CREATED, RES_UPDATED
+from lakesuperior.model.ldpr import Ldpr
+
+
+@pytest.fixture(scope='module')
+def random_uuid():
+    return str(uuid.uuid4())
+
+
+@pytest.mark.usefixtures('db')
+class TestResourceApi:
+    '''
+    Test interaction with the Resource API.
+    '''
+    def test_nodes_exist(self):
+        """
+        Verify whether nodes exist or not.
+        """
+        assert rsrc_api.exists('/') is True
+        assert rsrc_api.exists('/{}'.format(uuid4())) is False
+
+
+    def test_get_root_node_metadata(self):
+        """
+        Get the root node metadata.
+
+        The ``dcterms:title`` property should NOT be included.
+        """
+        gr = rsrc_api.get_metadata('/')
+        assert isinstance(gr, Graph)
+        assert len(gr) == 9
+        assert gr[gr.identifier : nsc['rdf'].type : nsc['ldp'].Resource ]
+        assert not gr[gr.identifier : nsc['dcterms'].title : "Repository Root"]
+
+
+    def test_get_root_node(self):
+        """
+        Get the root node.
+
+        The ``dcterms:title`` property should be included.
+        """
+        rsrc = rsrc_api.get('/')
+        assert isinstance(rsrc, Ldpr)
+        gr = rsrc.imr
+        assert len(gr) == 10
+        assert gr[gr.identifier : nsc['rdf'].type : nsc['ldp'].Resource ]
+        assert gr[
+            gr.identifier : nsc['dcterms'].title : Literal('Repository Root')]
+
+
+    def test_get_nonexisting_node(self):
+        """
+        Get a non-existing node.
+        """
+        with pytest.raises(ResourceNotExistsError):
+            gr = rsrc_api.get('/{}'.format(uuid4()))
+
+
+    def test_create_from_graph(self):
+        """
+        Create a resource from a provided graph.
+        """
+        uid = '/rsrc_from_graph'
+        uri = nsc['fcres'][uid]
+        gr = Graph().parse(
+            data='<> a <http://ex.org/type#A> .', format='turtle',
+            publicID=uri)
+        #pdb.set_trace()
+        evt = rsrc_api.create_or_replace(uid, init_gr=gr)
+
+        rsrc = rsrc_api.get(uid)
+        assert rsrc.imr[
+                rsrc.uri : nsc['rdf'].type : URIRef('http://ex.org/type#A')]
+        assert rsrc.imr[
+                rsrc.uri : nsc['rdf'].type : nsc['ldp'].RDFSource]
+
+
+    def test_create_from_rdf_stream(self):
+        """
+        Create a resource from a RDF stream (Turtle).
+
+        This is the same method used by the LDP endpoint.
+        """
+        uid = '/rsrc_from_stream'
+        uri = nsc['fcres'][uid]
+        stream = BytesIO(b'<> a <http://ex.org/type#B> .')
+        #pdb.set_trace()
+        evt = rsrc_api.create_or_replace(
+            uid, stream=stream, mimetype='text/turtle')
+
+        rsrc = rsrc_api.get(uid)
+        assert rsrc.imr[
+                rsrc.uri : nsc['rdf'].type : URIRef('http://ex.org/type#B')]
+        assert rsrc.imr[
+                rsrc.uri : nsc['rdf'].type : nsc['ldp'].RDFSource]
+
+
+    def test_replace_rsrc(self):
+        uid = '/test_replace'
+        uri = nsc['fcres'][uid]
+        gr1 = Graph().parse(
+            data='<> a <http://ex.org/type#A> .', format='turtle',
+            publicID=uri)
+        evt = rsrc_api.create_or_replace(uid, init_gr=gr1)
+        assert evt == RES_CREATED
+
+        rsrc = rsrc_api.get(uid)
+        assert rsrc.imr[
+                rsrc.uri : nsc['rdf'].type : URIRef('http://ex.org/type#A')]
+        assert rsrc.imr[
+                rsrc.uri : nsc['rdf'].type : nsc['ldp'].RDFSource]
+
+        gr2 = Graph().parse(
+            data='<> a <http://ex.org/type#B> .', format='turtle',
+            publicID=uri)
+        #pdb.set_trace()
+        evt = rsrc_api.create_or_replace(uid, init_gr=gr2)
+        assert evt == RES_UPDATED
+
+        rsrc = rsrc_api.get(uid)
+        assert not rsrc.imr[
+                rsrc.uri : nsc['rdf'].type : URIRef('http://ex.org/type#A')]
+        assert rsrc.imr[
+                rsrc.uri : nsc['rdf'].type : URIRef('http://ex.org/type#B')]
+        assert rsrc.imr[
+                rsrc.uri : nsc['rdf'].type : nsc['ldp'].RDFSource]