X-Git-Url: https://gerrit.o-ran-sc.org/r/gitweb?a=blobdiff_plain;ds=sidebyside;f=ricxappframe%2Fxapp_sdl.py;h=f4fd1806a82017b7031814d90e48adf428cdc18b;hb=e0de19a1c51e607b7e7a4a71a326a09486583539;hp=f8f2efc91e22f36e11774b082d30446b058f7fd1;hpb=bbc9028aa34ae48e7806596cd05fbe7a5bfd7fb8;p=ric-plt%2Fxapp-frame-py.git diff --git a/ricxappframe/xapp_sdl.py b/ricxappframe/xapp_sdl.py index f8f2efc..f4fd180 100644 --- a/ricxappframe/xapp_sdl.py +++ b/ricxappframe/xapp_sdl.py @@ -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 testing; e.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 -------