Updated alpine builder image to 4.6.0 in examples
[ric-plt/xapp-frame-py.git] / ricxappframe / xapp_sdl.py
index af41aab..f4fd180 100644 (file)
@@ -1,6 +1,3 @@
-"""
-sdl functionality
-"""
 # ==================================================================================
 #       Copyright (c) 2020 Nokia
 #       Copyright (c) 2020 AT&T Intellectual Property.
@@ -18,6 +15,9 @@ sdl functionality
 #   limitations under the License.
 # ==================================================================================
 
+"""
+sdl functionality
+"""
 
 import msgpack
 from ricsdl.syncstorage import SyncStorage
@@ -25,13 +25,12 @@ from ricsdl.syncstorage import SyncStorage
 
 class SDLWrapper:
     """
-    This is a wrapper around the SDL Python interface.
-
-    We do not embed the below directly in the Xapp classes because this SDL wrapper is useful for other python apps, for example A1 Mediator uses this verbatim.
-    Therefore, we leave this here as a seperate instantiable object so it can be used outside of xapps too
-    One could argue this get moved into *sdl itself*.
+    Provides convenient wrapper methods for using the SDL Python interface.
+    Optionally uses msgpack for binary (de)serialization:
+    see https://msgpack.org/index.html
 
-    We currently use msgpack for binary (de)serialization: https://msgpack.org/index.html
+    Published as a standalone module (and kept separate from the Xapp
+    framework classes) so these features can be used outside Xapps.
     """
 
     def __init__(self, use_fake_sdl=False):
@@ -40,9 +39,13 @@ class SDLWrapper:
 
         Parameters
         ----------
-        use_fake_sdl: bool
-            if this is True (default: False), then SDLs "fake dict backend" is used, which is very useful for testing since it allows you to use SDL without any SDL or Redis deployed at all.
-            This can be used while developing your xapp, and also for monkeypatching during unit testing (e.g., the xapp framework unit tests do this)
+        use_fake_sdl: bool (optional, default False)
+            if this is True, then use SDL's in-memory backend,
+            which is very useful for testing since it allows use
+            of SDL without a running SDL or Redis instance.
+            This can be used while developing an xapp and also
+            for monkeypatching during unit testing; e.g., the xapp
+            framework unit tests do this.
         """
         if use_fake_sdl:
             self._sdl = SyncStorage(fake_db_backend="dict")
@@ -51,96 +54,599 @@ class SDLWrapper:
 
     def set(self, ns, key, value, usemsgpack=True):
         """
-        set a key
+        Stores a key-value pair,
+        optionally serializing the value to bytes using msgpack.
 
-       NOTE: I am down for a discussion about whether usemsgpack should *default* to True or False here. This seems like a usage statistic question (that we don't have enough data for yet). Are more uses for an xapp to write/read their own data, or will more xapps end up reading data written by some other thing? I think it's too early to know this. So we go with True as the very first user of this, a1, does this. I'm open to changing this default to False later with evidence.
+        TODO: discuss whether usemsgpack should *default* to True or
+        False here. This seems like a usage statistic question (that we
+        don't have enough data for yet). Are more uses for an xapp to
+        write/read their own data, or will more xapps end up reading data
+        written by some other thing? I think it's too early to know.
 
         Parameters
         ----------
         ns: string
-           the sdl namespace
+            SDL namespace
         key: string
-            the sdl key
+            SDL key
         value:
-            if usemsgpack is True, value can be anything serializable by msgpack
-            if usemsgpack is False, value must be bytes
-        usemsgpack: boolean (optional)
-            determines whether the value is serialized using msgpack
+            Object or byte array to store.  See the `usemsgpack` parameter.
+        usemsgpack: boolean (optional, default is True)
+            Determines whether the value is serialized using msgpack before storing.
+            If usemsgpack is True, the msgpack function `packb` is invoked
+            on the value to yield a byte array that is then sent to SDL.
+            Stated differently, if usemsgpack is True, the value can be anything
+            that is serializable by msgpack.
+            If usemsgpack is False, the value must be bytes.
         """
         if usemsgpack:
-            self._sdl.set(ns, {key: msgpack.packb(value, use_bin_type=True)})
-        else:
-            self._sdl.set(ns, {key: value})
+            value = msgpack.packb(value, use_bin_type=True)
+        self._sdl.set(ns, {key: value})
+
+    def set_if(self, ns, key, old_value, new_value, usemsgpack=True):
+        """
+        Conditionally modify the value of a key if the current value in data storage matches the
+        user's last known value.
+
+        Parameters
+        ----------
+        ns: string
+            SDL namespace
+        key: string
+            SDL key
+        old_value:
+            Lask known object or byte array.  See the `usemsgpack` parameter.
+        new_value:
+            Object or byte array to be written.  See the `usemsgpack` parameter.
+        usemsgpack: boolean (optional, default is True)
+            Determines whether the value is serialized using msgpack before storing.
+            If usemsgpack is True, the msgpack function `packb` is invoked
+            on the value to yield a byte array that is then sent to SDL.
+            Stated differently, if usemsgpack is True, the value can be anything
+            that is serializable by msgpack.
+            If usemsgpack is False, the value must be bytes.
+
+        Returns
+        -------
+        bool
+            True for successful modification, false if the user's last known data did not
+            match the current value in data storage.
+        """
+        if usemsgpack:
+            old_value = msgpack.packb(old_value, use_bin_type=True)
+            new_value = msgpack.packb(new_value, use_bin_type=True)
+        return self._sdl.set_if(ns, key, old_value, new_value)
+
+    def set_if_not_exists(self, ns, key, value, usemsgpack=True):
+        """
+        Write data to SDL storage if key does not exist.
+
+        Parameters
+        ----------
+        ns: string
+            SDL namespace
+        key: string
+            SDL key
+        value:
+            Object or byte array to store.  See the `usemsgpack` parameter.
+        usemsgpack: boolean (optional, default is True)
+            Determines whether the value is serialized using msgpack before storing.
+            If usemsgpack is True, the msgpack function `packb` is invoked
+            on the value to yield a byte array that is then sent to SDL.
+            Stated differently, if usemsgpack is True, the value can be anything
+            that is serializable by msgpack.
+            If usemsgpack is False, the value must be bytes.
+
+        Returns
+        -------
+        bool
+            True for successful modification, false if the user's last known data did not
+            match the current value in data storage.
+        """
+        if usemsgpack:
+            value = msgpack.packb(value, use_bin_type=True)
+        return self._sdl.set_if_not_exists(ns, key, value)
 
     def get(self, ns, key, usemsgpack=True):
         """
-        get a key
+        Gets the value for the specified namespace and key,
+        optionally deserializing stored bytes using msgpack.
 
         Parameters
         ----------
         ns: string
-           the sdl namespace
+            SDL namespace
         key: string
-            the sdl key
-        usemsgpack: boolean (optional)
-            if usemsgpack is True, the value is deserialized using msgpack
-            if usemsgpack is False, the value is returned as raw bytes
+            SDL key
+        usemsgpack: boolean (optional, default is True)
+            If usemsgpack is True, the byte array stored by SDL is deserialized
+            using msgpack to yield the original object that was stored.
+            If usemsgpack is False, the byte array stored by SDL is returned
+            without further processing.
 
         Returns
         -------
-        None (if not exist) or see above; depends on usemsgpack
+        Value
+            See the usemsgpack parameter for an explanation of the returned value type.
+            Answers None if the key is not found.
         """
+        result = None
         ret_dict = self._sdl.get(ns, {key})
         if key in ret_dict:
+            result = ret_dict[key]
             if usemsgpack:
-                return msgpack.unpackb(ret_dict[key], raw=False)
-            return ret_dict[key]
+                result = msgpack.unpackb(result, raw=False)
+        return result
+
+    def find_keys(self, ns, prefix):
+        """
+        Find all keys matching search pattern under the namespace.
+
+        Parameters
+        ----------
+        ns: string
+            SDL namespace
+        prefix: string
+            Key search pattern
 
-        return None
+        Returns
+        -------
+        keys: list
+            A list of found keys.
+        """
+        return self._sdl.find_keys(ns, f"{prefix}*")
 
     def find_and_get(self, ns, prefix, usemsgpack=True):
         """
-        get all k v pairs that start with prefix
+        Gets all key-value pairs in the specified namespace
+        with keys that start with the specified prefix,
+        optionally deserializing stored bytes using msgpack.
 
         Parameters
         ----------
         ns: string
-           the sdl namespace
-        key: string
-            the sdl key
+           SDL namespace
         prefix: string
-            the prefix
-        usemsgpack: boolean (optional)
-            if usemsgpack is True, the value returned is a dict where each value has been deserialized using msgpack
-            if usemsgpack is False, the value returned is as a dict mapping keys to raw bytes
+            the key prefix
+        usemsgpack: boolean (optional, default is True)
+            If usemsgpack is True, every byte array stored by SDL is deserialized
+            using msgpack to yield the original value that was stored.
+            If usemsgpack is False, every byte array stored by SDL is returned
+            without further processing.
 
         Returns
         -------
-        {} (if no keys match) or see above; depends on usemsgpack
+        Dictionary of key-value pairs
+            Each key has the specified prefix.
+            See the usemsgpack parameter for an explanation of the returned value types.
+            Answers an empty dictionary if no keys matched the prefix.
         """
 
         # note: SDL "*" usage is inconsistent with real python regex, where it would be ".*"
-        ret_dict = self._sdl.find_and_get(ns, "{0}*".format(prefix))
+        ret_dict = self._sdl.find_and_get(ns, f"{prefix}*")
         if usemsgpack:
-            return {k: msgpack.unpackb(v, raw=False) for k, v in ret_dict.items()}
+            ret_dict = {k: msgpack.unpackb(v, raw=False) for k, v in ret_dict.items()}
         return ret_dict
 
     def delete(self, ns, key):
         """
-        delete a key
+        Deletes the key-value pair with the specified key in the specified namespace.
 
         Parameters
         ----------
         ns: string
-           the sdl namespace
+           SDL namespace
         key: string
-            the sdl key
+            SDL key
         """
         self._sdl.remove(ns, {key})
 
+    def delete_if(self, ns, key, value, usemsgpack=True):
+        """
+        Conditionally remove data from SDL storage if the current data value matches the user's
+        last known value.
+
+        Parameters
+        ----------
+        ns: string
+            SDL namespace
+        key: string
+            SDL key
+        value:
+            Object or byte array to store.  See the `usemsgpack` parameter.
+        usemsgpack: boolean (optional, default is True)
+            Determines whether the value is serialized using msgpack before storing.
+            If usemsgpack is True, the msgpack function `packb` is invoked
+            on the value to yield a byte array that is then sent to SDL.
+            Stated differently, if usemsgpack is True, the value can be anything
+            that is serializable by msgpack.
+            If usemsgpack is False, the value must be bytes.
+
+        Returns
+        -------
+        bool
+            True if successful removal, false if the user's last known data did not match the
+            current value in data storage.
+        """
+        if usemsgpack:
+            value = msgpack.packb(value, use_bin_type=True)
+        return self._sdl.remove_if(ns, key, value)
+
+    def add_member(self, ns, group, member, usemsgpack=True):
+        """
+        Add new members to a SDL group under the namespace.
+
+        Parameters
+        ----------
+        ns: string
+            SDL namespace
+        group: string
+            group name
+        member:
+            member to be added
+        usemsgpack: boolean (optional, default is True)
+            Determines whether the member is serialized using msgpack before storing.
+            If usemsgpack is True, the msgpack function `packb` is invoked
+            on the member to yield a byte array that is then sent to SDL.
+            Stated differently, if usemsgpack is True, the member can be anything
+            that is serializable by msgpack.
+            If usemsgpack is False, the member must be bytes.
+        """
+        if usemsgpack:
+            member = msgpack.packb(member, use_bin_type=True)
+        self._sdl.add_member(ns, group, {member})
+
+    def remove_member(self, ns, group, member, usemsgpack=True):
+        """
+        Remove members from a SDL group.
+
+        Parameters
+        ----------
+        ns: string
+            SDL namespace
+        group: string
+            group name
+        member:
+            member to be removed
+        usemsgpack: boolean (optional, default is True)
+            Determines whether the member is serialized using msgpack before storing.
+            If usemsgpack is True, the msgpack function `packb` is invoked
+            on the member to yield a byte array that is then sent to SDL.
+            Stated differently, if usemsgpack is True, the member can be anything
+            that is serializable by msgpack.
+            If usemsgpack is False, the member must be bytes.
+        """
+        if usemsgpack:
+            member = msgpack.packb(member, use_bin_type=True)
+        self._sdl.remove_member(ns, group, {member})
+
+    def remove_group(self, ns, group):
+        """
+        Remove a SDL group along with its members.
+
+        Parameters
+        ----------
+        ns: string
+            SDL namespace
+        group: string
+            group name to remove
+        usemsgpack: boolean (optional, default is True)
+            Determines whether the member is serialized using msgpack before storing.
+            If usemsgpack is True, the msgpack function `packb` is invoked
+            on the member to yield a byte array that is then sent to SDL.
+            Stated differently, if usemsgpack is True, the member can be anything
+            that is serializable by msgpack.
+            If usemsgpack is False, the member must be bytes.
+        """
+        self._sdl.remove_group(ns, group)
+
+    def get_members(self, ns, group, usemsgpack=True):
+        """
+        Get all the members of a SDL group.
+
+        Parameters
+        ----------
+        ns: string
+            SDL namespace
+        group: string
+            group name to retrive
+        usemsgpack: boolean (optional, default is True)
+            Determines whether the member is serialized using msgpack before storing.
+            If usemsgpack is True, the msgpack function `packb` is invoked
+            on the member to yield a byte array that is then sent to SDL.
+            Stated differently, if usemsgpack is True, the member can be anything
+            that is serializable by msgpack.
+            If usemsgpack is False, the member must be bytes.
+
+        Returns
+        -------
+        Set[str] or Set[bytes]
+            A set of the members of the group.
+        None
+        """
+        ret_set = self._sdl.get_members(ns, group)
+        if usemsgpack:
+            ret_set = {msgpack.unpackb(m, raw=False) for m in ret_set}
+        return ret_set
+
+    def is_member(self, ns, group, member, usemsgpack=True):
+        """
+        Validate if a given member is in the SDL group.
+
+        Parameters
+        ----------
+        ns: string
+            SDL namespace
+        group: string
+            group name
+        member:
+            member to validate
+        usemsgpack: boolean (optional, default is True)
+            Determines whether the member is serialized using msgpack before storing.
+            If usemsgpack is True, the msgpack function `packb` is invoked
+            on the member to yield a byte array that is then sent to SDL.
+            Stated differently, if usemsgpack is True, the member can be anything
+            that is serializable by msgpack.
+            If usemsgpack is False, the member must be bytes.
+
+        Returns
+        -------
+        bool
+            True if member was in the group, false otherwise.
+        """
+        if usemsgpack:
+            member = msgpack.packb(member, use_bin_type=True)
+        return self._sdl.is_member(ns, group, member)
+
+    def group_size(self, ns, group):
+        """
+        Return the number of members in a group.
+        If the group does not exist, value 0 is returned.
+
+        Parameters
+        ----------
+        ns: string
+            SDL namespace
+        group: string
+            group name to retrive size
+        usemsgpack: boolean (optional, default is True)
+            Determines whether the member is serialized using msgpack before storing.
+            If usemsgpack is True, the msgpack function `packb` is invoked
+            on the member to yield a byte array that is then sent to SDL.
+            Stated differently, if usemsgpack is True, the member can be anything
+            that is serializable by msgpack.
+            If usemsgpack is False, the member must be bytes.
+
+        Returns
+        -------
+        int
+            Number of members in a group.
+        """
+        return self._sdl.group_size(ns, group)
+
+    def set_and_publish(self, ns, channel, event, key, value, usemsgpack=True):
+        """
+        Publish event to channel after writing data.
+
+        Parameters
+        ----------
+        ns: string
+            SDL namespace
+        channel: string
+            channel to publish event
+        event: string
+            published message
+        key: string
+            SDL key
+        value:
+            Object or byte array to store.  See the `usemsgpack` parameter.
+        usemsgpack: boolean (optional, default is True)
+            Determines whether the value is serialized using msgpack before storing.
+            If usemsgpack is True, the msgpack function `packb` is invoked
+            on the value to yield a byte array that is then sent to SDL.
+            Stated differently, if usemsgpack is True, the value can be anything
+            that is serializable by msgpack.
+            If usemsgpack is False, the value must be bytes.
+        """
+        if usemsgpack:
+            value = msgpack.packb(value, use_bin_type=True)
+        self._sdl.set_and_publish(ns, {channel: event}, {key: value})
+
+    def set_if_and_publish(self, ns, channel, event, key, old_value, new_value, usemsgpack=True):
+        """
+        Publish event to channel after conditionally modifying the value of a key if the
+        current value in data storage matches the user's last known value.
+
+        Parameters
+        ----------
+        ns: string
+            SDL namespace
+        channel: string
+            channel to publish event
+        event: string
+            published message
+        key: string
+            SDL key
+        old_value:
+            Lask known object or byte array.  See the `usemsgpack` parameter.
+        new_value:
+            Object or byte array to be written.  See the `usemsgpack` parameter.
+        usemsgpack: boolean (optional, default is True)
+            Determines whether the old_value & new_value is serialized using msgpack before storing.
+            If usemsgpack is True, the msgpack function `packb` is invoked
+            on the old_value & new_value to yield a byte array that is then sent to SDL.
+            Stated differently, if usemsgpack is True, the old_value & new_value can be anything
+            that is serializable by msgpack.
+            If usemsgpack is False, the old_value & new_value must be bytes.
+
+        Returns
+        -------
+        bool
+            True for successful modification, false if the user's last known data did not
+            match the current value in data storage.
+        """
+        if usemsgpack:
+            old_value = msgpack.packb(old_value, use_bin_type=True)
+            new_value = msgpack.packb(new_value, use_bin_type=True)
+        return self._sdl.set_if_and_publish(ns, {channel: event}, key, old_value, new_value)
+
+    def set_if_not_exists_and_publish(self, ns, channel, event, key, value, usemsgpack=True):
+        """
+        Publish event to channel after writing data to SDL storage if key does not exist.
+
+        Parameters
+        ----------
+        ns: string
+            SDL namespace
+        channel: string
+            channel to publish event
+        event: string
+            published message
+        key: string
+            SDL key
+        value:
+            Object or byte array to store.  See the `usemsgpack` parameter.
+        usemsgpack: boolean (optional, default is True)
+            Determines whether the value is serialized using msgpack before storing.
+            If usemsgpack is True, the msgpack function `packb` is invoked
+            on the value to yield a byte array that is then sent to SDL.
+            Stated differently, if usemsgpack is True, the value can be anything
+            that is serializable by msgpack.
+            If usemsgpack is False, the value must be bytes.
+
+        Returns
+        -------
+        bool
+            True if key didn't exist yet and set operation was executed, false if key already
+            existed and thus its value was left untouched.
+        """
+        if usemsgpack:
+            value = msgpack.packb(value, use_bin_type=True)
+        return self._sdl.set_if_not_exists_and_publish(ns, {channel: event}, key, value)
+
+    def remove_and_publish(self, ns, channel, event, key):
+        """
+        Publish event to channel after removing data.
+
+        Parameters
+        ----------
+        ns: string
+            SDL namespace
+        channel: string
+            channel to publish event
+        event: string
+            published message
+        key: string
+            SDL key
+        """
+        self._sdl.remove_and_publish(ns, {channel: event}, {key})
+
+    def remove_if_and_publish(self, ns, channel, event, key, value, usemsgpack=True):
+        """
+        Publish event to channel after removing key and its data from database if the
+        current data value is expected one.
+
+        Parameters
+        ----------
+        ns: string
+            SDL namespace
+        channel: string
+            channel to publish event
+        event: string
+            published message
+        key: string
+            SDL key
+        value:
+            Object or byte array to store.  See the `usemsgpack` parameter.
+        usemsgpack: boolean (optional, default is True)
+            Determines whether the value is serialized using msgpack before storing.
+            If usemsgpack is True, the msgpack function `packb` is invoked
+            on the value to yield a byte array that is then sent to SDL.
+            Stated differently, if usemsgpack is True, the value can be anything
+            that is serializable by msgpack.
+            If usemsgpack is False, the value must be bytes.
+
+        Returns
+        -------
+        bool
+            True if successful removal, false if the user's last known data did not match the
+            current value in data storage.
+        """
+        if usemsgpack:
+            value = msgpack.packb(value, use_bin_type=True)
+        return self._sdl.remove_if_and_publish(ns, {channel: event}, key, value)
+
+    def remove_all_and_publish(self, ns, channel, event):
+        """
+        Publish event to channel after removing all keys under the namespace.
+
+        Parameters
+        ----------
+        ns: string
+            SDL namespace
+        channel: string
+            channel to publish event
+        event: string
+            published message
+        """
+        self._sdl.remove_all_and_publish(ns, {channel: event})
+
+    def subscribe_channel(self, ns, cb, channel):
+        """
+        Subscribes the client to the specified channels.
+
+        Parameters
+        ----------
+        ns: string
+            SDL namespace
+        cb:
+            A function that is called when an event on channel is received.
+        channel: string
+            channel to subscribe
+        """
+        self._sdl.subscribe_channel(ns, cb, {channel})
+
+    def unsubscribe_channel(self, ns, channel):
+        """
+        unsubscribe_channel removes subscription from one or several channels.
+
+        Parameters
+        ----------
+        ns: string
+            SDL namespace
+        channel: string
+            channel to unsubscribe
+        """
+        self._sdl.unsubscribe_channel(ns, {channel})
+
+    def start_event_listener(self):
+        """
+        start_event_listener creates an event loop in a separate thread for handling
+        events from subscriptions. The registered callback function will be called
+        when an event is received.
+        """
+        self._sdl.start_event_listener()
+
+    def handle_events(self):
+        """
+        handle_events is a non-blocking function that returns a tuple containing channel
+        name and a list of message(s) received from an event. The registered callback
+        function will still be called when an event is received.
+
+        This function is called if SDL user decides to handle notifications in its own
+        event loop. Calling this function after start_event_listener raises an exception.
+        If there are no notifications, these returns None.
+
+        Returns
+        -------
+        Tuple:
+            (channel: str, message: list of str)
+        """
+        return self._sdl.handle_events()
+
     def healthcheck(self):
         """
-        checks if the sdl connection is healthy
+        Checks if the sdl connection is healthy.
 
         Returns
         -------