Quellcode durchsuchen

PATCH and other improvements.

* Allow delta patching in Python API
* Rename Ldpr._modify_rsrc to modify
* Rename Ldpr._check_mgd_terms to chk_mgd_terms
* Remove method redundancy in Ldpr
* Change check_mgd_terms to use sets instead of graphs
Stefano Cossu vor 6 Jahren
Ursprung
Commit
72648009eb
4 geänderte Dateien mit 200 neuen und 119 gelöschten Zeilen
  1. 33 13
      lakesuperior/api/resource.py
  2. 1 1
      lakesuperior/model/ldp_factory.py
  3. 101 105
      lakesuperior/model/ldpr.py
  4. 65 0
      tests/test_resource_api.py

+ 33 - 13
lakesuperior/api/resource.py

@@ -7,14 +7,14 @@ from threading import Lock, Thread
 
 import arrow
 
-from rdflib import Literal
+from rdflib import Graph, Literal, URIRef
 from rdflib.namespace import XSD
 
 from lakesuperior.config_parser import config
 from lakesuperior.exceptions import (
         InvalidResourceError, ResourceNotExistsError, TombstoneError)
 from lakesuperior.env import env
-from lakesuperior.globals import RES_DELETED
+from lakesuperior.globals import RES_DELETED, RES_UPDATED
 from lakesuperior.model.ldp_factory import LDP_NR_TYPE, LdpFactory
 from lakesuperior.store.ldp_rs.lmdb_store import TxnManager
 
@@ -220,8 +220,6 @@ def create_or_replace(uid, **kwargs):
     Create or replace a resource with a specified UID.
 
     :param string uid: UID of the resource to be created or updated.
-    :param BytesIO stream: Content stream. If empty, an empty container is
-        created.
     :param \*\*kwargs: Other parameters are passed to the
         :py:meth:`~lakesuperior.model.ldp_factory.LdpFactory.from_provided`
         method.
@@ -229,9 +227,7 @@ def create_or_replace(uid, **kwargs):
     :rtype: str
     :return: Event type: whether the resource was created or updated.
     """
-    rsrc = LdpFactory.from_provided(uid, **kwargs)
-
-    return rsrc.create_or_replace()
+    return LdpFactory.from_provided(uid, **kwargs).create_or_replace()
 
 
 @transaction(True)
@@ -241,19 +237,43 @@ def update(uid, update_str, is_metadata=False):
 
     :param string uid: Resource UID.
     :param string update_str: SPARQL-Update statements.
-    :param bool is_metadata: Whether the resource metadata is being updated.
-        If False, and the resource being updated is a LDP-NR, an error is
-        raised.
+    :param bool is_metadata: Whether the resource metadata are being updated.
+
+    :raise InvalidResourceError: If ``is_metadata`` is False and the resource
+        being updated is a LDP-NR.
     """
-    rsrc = LdpFactory.from_stored(uid)
+    # FCREPO is lenient here and Hyrax requires it.
+    rsrc = LdpFactory.from_stored(uid, handling='lenient')
     if LDP_NR_TYPE in rsrc.ldp_types and not is_metadata:
-        raise InvalidResourceError(uid)
+        raise InvalidResourceError(
+                'Cannot use this method to update an LDP-NR content.')
 
-    rsrc.sparql_update(update_str)
+    delta = rsrc.sparql_delta(update_str)
+    rsrc.modify(RES_UPDATED, *delta)
 
     return rsrc
 
 
+@transaction(True)
+def update_delta(uid, remove_trp, add_trp):
+    """
+    Update a resource graph (LDP-RS or LDP-NR) with sets of add/remove triples.
+
+    A set of triples to add and/or a set of triples to remove may be provided.
+
+    :param string uid: Resource UID.
+    :param set(tuple(rdflib.term.Identifier)) remove_trp: Triples to
+        remove, as 3-tuples of RDFLib terms.
+    :param set(tuple(rdflib.term.Identifier)) add_trp: Triples to
+        add, as 3-tuples of RDFLib terms.
+    """
+    rsrc = LdpFactory.from_stored(uid)
+    remove_trp = rsrc.check_mgd_terms(remove_trp)
+    add_trp = rsrc.check_mgd_terms(add_trp)
+
+    return rsrc.modify(RES_UPDATED, remove_trp, add_trp)
+
+
 @transaction(True)
 def create_version(uid, ver_uid):
     """

+ 1 - 1
lakesuperior/model/ldp_factory.py

@@ -128,7 +128,7 @@ class LdpFactory:
                 raise IncompatibleLdpTypeError(uid, mimetype)
 
             if kwargs.get('handling', 'strict') != 'none':
-                inst._check_mgd_terms(inst.provided_imr)
+                inst.check_mgd_terms(inst.provided_imr)
 
         else:
             # Resource is a LDP-NR.

+ 101 - 105
lakesuperior/model/ldpr.py

@@ -377,7 +377,7 @@ class Ldpr(metaclass=ABCMeta):
             set(self.provided_imr) |
             self._containment_rel(create))
 
-        self._modify_rsrc(ev_type, remove_trp, add_trp)
+        self.modify(ev_type, remove_trp, add_trp)
         new_gr = Graph(identifier=self.uri)
         for trp in add_trp:
             new_gr.add(trp)
@@ -414,7 +414,7 @@ class Ldpr(metaclass=ABCMeta):
                 (self.uri, nsc['fcrepo'].created, env.timestamp_term),
             }
 
-        self._modify_rsrc(RES_DELETED, remove_trp, add_trp)
+        self.modify(RES_DELETED, remove_trp, add_trp)
 
         if inbound:
             for ib_rsrc_uri in self.imr.subjects(None, self.uri):
@@ -422,7 +422,7 @@ class Ldpr(metaclass=ABCMeta):
                 ib_rsrc = Ldpr(ib_rsrc_uri)
                 # To preserve inbound links in history, create a snapshot
                 ib_rsrc.create_rsrc_snapshot(uuid4())
-                ib_rsrc._modify_rsrc(RES_UPDATED, remove_trp)
+                ib_rsrc.modify(RES_UPDATED, remove_trp)
 
         return RES_DELETED
 
@@ -481,7 +481,7 @@ class Ldpr(metaclass=ABCMeta):
             (self.uri, nsc['fcrepo'].hasVersion, ver_uri),
             (self.uri, nsc['fcrepo'].hasVersions, nsc['fcres'][vers_uid]),
         }
-        self._modify_rsrc(RES_UPDATED, add_trp=rsrc_add_gr)
+        self.modify(RES_UPDATED, add_trp=rsrc_add_gr)
 
         return ver_uid
 
@@ -518,7 +518,7 @@ class Ldpr(metaclass=ABCMeta):
             laz_gr.add((self.uri, RDF.type, nsc['fcrepo'].Container))
 
         laz_set = set(laz_gr) | self._containment_rel()
-        self._modify_rsrc(RES_CREATED, tstone_trp, laz_set)
+        self.modify(RES_CREATED, tstone_trp, laz_set)
 
         return self.uri
 
@@ -566,6 +566,99 @@ class Ldpr(metaclass=ABCMeta):
         return self.create_or_replace(create_only=False)
 
 
+    def check_mgd_terms(self, trp):
+        """
+        Check whether server-managed terms are in a RDF payload.
+
+        :param rdflib.Graph trp: The graph to validate.
+        """
+        subjects = {t[0] for t in trp}
+        offending_subjects = subjects & srv_mgd_subjects
+        if offending_subjects:
+            if self.handling == 'strict':
+                raise ServerManagedTermError(offending_subjects, 's')
+            else:
+                for s in offending_subjects:
+                    logger.info('Removing offending subj: {}'.format(s))
+                    for t in trp:
+                        if t[0] == s:
+                            trp.remove(t)
+
+        predicates = {t[1] for t in trp}
+        offending_predicates = predicates & srv_mgd_predicates
+        # Allow some predicates if the resource is being created.
+        if offending_predicates:
+            if self.handling == 'strict':
+                raise ServerManagedTermError(offending_predicates, 'p')
+            else:
+                for p in offending_predicates:
+                    logger.info('Removing offending pred: {}'.format(p))
+                    for t in trp:
+                        if t[1] == p:
+                            trp.remove(t)
+
+        types = {t[2] for t in trp if t[1] == RDF.type}
+        offending_types = types & srv_mgd_types
+        if not self.is_stored:
+            offending_types -= self.smt_allow_on_create
+        if offending_types:
+            if self.handling == 'strict':
+                raise ServerManagedTermError(offending_types, 't')
+            else:
+                for to in offending_types:
+                    logger.info('Removing offending type: {}'.format(to))
+                    for t in trp:
+                        if t[1] == RDF.type and t[2] == to:
+                            trp.remove(t)
+
+        #logger.debug('Sanitized graph: {}'.format(trp.serialize(
+        #    format='turtle').decode('utf-8')))
+        return trp
+
+
+    def sparql_delta(self, q):
+        """
+        Calculate the delta obtained by a SPARQL Update operation.
+
+        This is a critical component of the SPARQL update prcess and does a
+        couple of things:
+
+        1. It ensures that no resources outside of the subject of the request
+        are modified (e.g. by variable subjects)
+        2. It verifies that none of the terms being modified is server managed.
+
+        This method extracts an in-memory copy of the resource and performs the
+        query on that once it has checked if any of the server managed terms is
+        in the delta. If it is, it raises an exception.
+
+        NOTE: This only checks if a server-managed term is effectively being
+        modified. If a server-managed term is present in the query but does not
+        cause any change in the updated resource, no error is raised.
+
+        :rtype: tuple(rdflib.Graph)
+        :return: Remove and add graphs. These can be used
+        with ``BaseStoreLayout.update_resource`` and/or recorded as separate
+        events in a provenance tracking system.
+        """
+        logger.debug('Provided SPARQL query: {}'.format(q))
+        pre_gr = self.imr
+
+        post_gr = pre_gr | Graph()
+        post_gr.update(q)
+
+        remove_gr, add_gr = self._dedup_deltas(pre_gr, post_gr)
+
+        #logger.debug('Removing: {}'.format(
+        #    remove_gr.serialize(format='turtle').decode('utf8')))
+        #logger.debug('Adding: {}'.format(
+        #    add_gr.serialize(format='turtle').decode('utf8')))
+
+        remove_trp = self.check_mgd_terms(set(remove_gr))
+        add_trp = self.check_mgd_terms(set(add_gr))
+
+        return remove_trp, add_trp
+
+
     ## PROTECTED METHODS ##
 
     def _is_trp_managed(self, t):
@@ -580,7 +673,7 @@ class Ldpr(metaclass=ABCMeta):
             t[1] == RDF.type and t[2] in srv_mgd_types)
 
 
-    def _modify_rsrc(
+    def modify(
             self, ev_type, remove_trp=set(), add_trp=set()):
         """
         Low-level method to modify a graph for a single resource.
@@ -657,47 +750,6 @@ class Ldpr(metaclass=ABCMeta):
                         self.provided_imr.remove((None, None, o))
 
 
-    def _check_mgd_terms(self, gr):
-        """
-        Check whether server-managed terms are in a RDF payload.
-
-        :param rdflib.Graph gr: The graph to validate.
-        """
-        offending_subjects = set(gr.subjects()) & srv_mgd_subjects
-        if offending_subjects:
-            if self.handling == 'strict':
-                raise ServerManagedTermError(offending_subjects, 's')
-            else:
-                for s in offending_subjects:
-                    logger.info('Removing offending subj: {}'.format(s))
-                    gr.remove((s, None, None))
-
-        offending_predicates = set(gr.predicates()) & srv_mgd_predicates
-        # Allow some predicates if the resource is being created.
-        if offending_predicates:
-            if self.handling == 'strict':
-                raise ServerManagedTermError(offending_predicates, 'p')
-            else:
-                for p in offending_predicates:
-                    logger.info('Removing offending pred: {}'.format(p))
-                    gr.remove((None, p, None))
-
-        offending_types = set(gr.objects(predicate=RDF.type)) & srv_mgd_types
-        if not self.is_stored:
-            offending_types -= self.smt_allow_on_create
-        if offending_types:
-            if self.handling == 'strict':
-                raise ServerManagedTermError(offending_types, 't')
-            else:
-                for t in offending_types:
-                    logger.info('Removing offending type: {}'.format(t))
-                    gr.remove((None, RDF.type, t))
-
-        #logger.debug('Sanitized graph: {}'.format(gr.serialize(
-        #    format='turtle').decode('utf-8')))
-        return gr
-
-
     def _add_srv_mgd_triples(self, create=False):
         """
         Add server-managed triples to a provided IMR.
@@ -784,7 +836,7 @@ class Ldpr(metaclass=ABCMeta):
             add_gr = Graph()
             add_gr.add(
                 (nsc['fcres'][parent_uid], nsc['ldp'].contains, self.uri))
-            parent_rsrc._modify_rsrc(RES_UPDATED, add_trp=add_gr)
+            parent_rsrc.modify(RES_UPDATED, add_trp=add_gr)
 
         # Direct or indirect container relationship.
         return self._add_ldp_dc_ic_rel(parent_rsrc)
@@ -842,62 +894,6 @@ class Ldpr(metaclass=ABCMeta):
                 logger.debug('Creating IC triples.')
 
             target_rsrc = LdpFactory.from_stored(rdfly.uri_to_uid(s))
-            target_rsrc._modify_rsrc(RES_UPDATED, add_trp={(s, p, o)})
+            target_rsrc.modify(RES_UPDATED, add_trp={(s, p, o)})
 
         return add_trp
-
-
-    def sparql_update(self, update_str):
-        """
-        Apply a SPARQL update to a resource.
-
-        :param str update_str: SPARQL-Update string. All URIs are local.
-        """
-        # FCREPO does that and Hyrax requires it.
-        self.handling = 'lenient'
-        delta = self._sparql_delta(update_str)
-
-        self._modify_rsrc(RES_UPDATED, *delta)
-
-
-    def _sparql_delta(self, q):
-        """
-        Calculate the delta obtained by a SPARQL Update operation.
-
-        This is a critical component of the SPARQL update prcess and does a
-        couple of things:
-
-        1. It ensures that no resources outside of the subject of the request
-        are modified (e.g. by variable subjects)
-        2. It verifies that none of the terms being modified is server managed.
-
-        This method extracts an in-memory copy of the resource and performs the
-        query on that once it has checked if any of the server managed terms is
-        in the delta. If it is, it raises an exception.
-
-        NOTE: This only checks if a server-managed term is effectively being
-        modified. If a server-managed term is present in the query but does not
-        cause any change in the updated resource, no error is raised.
-
-        :rtype: tuple(rdflib.Graph)
-        :return: Remove and add graphs. These can be used
-        with ``BaseStoreLayout.update_resource`` and/or recorded as separate
-        events in a provenance tracking system.
-        """
-        logger.debug('Provided SPARQL query: {}'.format(q))
-        pre_gr = self.imr
-
-        post_gr = pre_gr | Graph()
-        post_gr.update(q)
-
-        remove_gr, add_gr = self._dedup_deltas(pre_gr, post_gr)
-
-        #logger.debug('Removing: {}'.format(
-        #    remove_gr.serialize(format='turtle').decode('utf8')))
-        #logger.debug('Adding: {}'.format(
-        #    add_gr.serialize(format='turtle').decode('utf8')))
-
-        remove_gr = self._check_mgd_terms(remove_gr)
-        add_gr = self._check_mgd_terms(add_gr)
-
-        return set(remove_gr), set(add_gr)

+ 65 - 0
tests/test_resource_api.py

@@ -160,3 +160,68 @@ class TestResourceApi:
             rsrc_api.create_or_replace(uid_nr)
 
 
+    def test_delta_update(self):
+        """
+        Update a resource with two sets of add and remove triples.
+        """
+        uid = '/test_delta_patch'
+        uri = nsc['fcres'][uid]
+        init_trp = {
+            (URIRef(uri), nsc['rdf'].type, nsc['foaf'].Person),
+            (URIRef(uri), nsc['foaf'].name, Literal('Joe Bob')),
+        }
+        remove_trp = {
+            (URIRef(uri), nsc['rdf'].type, nsc['foaf'].Person),
+        }
+        add_trp = {
+            (URIRef(uri), nsc['rdf'].type, nsc['foaf'].Organization),
+        }
+
+        gr = Graph()
+        gr += init_trp
+        rsrc_api.create_or_replace(uid, graph=gr)
+        rsrc_api.update_delta(uid, remove_trp, add_trp)
+        rsrc = rsrc_api.get(uid)
+
+        assert rsrc.imr[
+                rsrc.uri : nsc['rdf'].type : nsc['foaf'].Organization]
+        assert rsrc.imr[rsrc.uri : nsc['foaf'].name : Literal('Joe Bob')]
+        assert not rsrc.imr[
+                rsrc.uri : nsc['rdf'].type : nsc['foaf'].Person]
+
+
+    def test_delta_update_wildcard(self):
+        """
+        Update a resource using wildcard modifiers.
+        """
+        uid = '/test_delta_patch_wc'
+        uri = nsc['fcres'][uid]
+        init_trp = {
+            (URIRef(uri), nsc['rdf'].type, nsc['foaf'].Person),
+            (URIRef(uri), nsc['foaf'].name, Literal('Joe Bob')),
+            (URIRef(uri), nsc['foaf'].name, Literal('Joe Average Bob')),
+            (URIRef(uri), nsc['foaf'].name, Literal('Joe 12oz Bob')),
+        }
+        remove_trp = {
+            (URIRef(uri), nsc['foaf'].name, None),
+        }
+        add_trp = {
+            (URIRef(uri), nsc['foaf'].name, Literal('Joan Knob')),
+        }
+
+        gr = Graph()
+        gr += init_trp
+        rsrc_api.create_or_replace(uid, graph=gr)
+        rsrc_api.update_delta(uid, remove_trp, add_trp)
+        rsrc = rsrc_api.get(uid)
+
+        assert rsrc.imr[
+                rsrc.uri : nsc['rdf'].type : nsc['foaf'].Person]
+        assert rsrc.imr[rsrc.uri : nsc['foaf'].name : Literal('Joan Knob')]
+        assert not rsrc.imr[rsrc.uri : nsc['foaf'].name : Literal('Joe Bob')]
+        assert not rsrc.imr[
+            rsrc.uri : nsc['foaf'].name : Literal('Joe Average Bob')]
+        assert not rsrc.imr[
+            rsrc.uri : nsc['foaf'].name : Literal('Joe 12oz Bob')]
+
+