Updated alpine builder image to 4.6.0 in examples
[ric-plt/xapp-frame-py.git] / ricxappframe / xapp_sdl.py
index f8f2efc..f4fd180 100644 (file)
@@ -25,16 +25,12 @@ from ricsdl.syncstorage import SyncStorage
 
 class SDLWrapper:
     """
-    This is a wrapper around the SDL Python interface.
+    Provides convenient wrapper methods for using the SDL Python interface.
+    Optionally uses msgpack for binary (de)serialization:
+    see https://msgpack.org/index.html
 
-    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*.
-
-    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):
@@ -43,13 +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 testinge.g., the xapp
+            framework unit tests do this.
         """
         if use_fake_sdl:
             self._sdl = SyncStorage(fake_db_backend="dict")
@@ -58,103 +54,599 @@ class SDLWrapper:
 
     def set(self, ns, key, value, usemsgpack=True):
         """
-        sets a key
+        Stores a key-value pair,
+        optionally serializing the value to bytes using msgpack.
 
         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
-        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.
+        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
 
-        return None
+    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
+
+        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
         -------