Merge "RIC:1060: Change in PTL"
[ric-plt/xapp-frame-py.git] / ricxappframe / xapp_sdl.py
1 # ==================================================================================
2 #       Copyright (c) 2020 Nokia
3 #       Copyright (c) 2020 AT&T Intellectual Property.
4 #
5 #   Licensed under the Apache License, Version 2.0 (the "License");
6 #   you may not use this file except in compliance with the License.
7 #   You may obtain a copy of the License at
8 #
9 #          http://www.apache.org/licenses/LICENSE-2.0
10 #
11 #   Unless required by applicable law or agreed to in writing, software
12 #   distributed under the License is distributed on an "AS IS" BASIS,
13 #   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 #   See the License for the specific language governing permissions and
15 #   limitations under the License.
16 # ==================================================================================
17
18 """
19 sdl functionality
20 """
21
22 import msgpack
23 from ricsdl.syncstorage import SyncStorage
24
25
26 class SDLWrapper:
27     """
28     Provides convenient wrapper methods for using the SDL Python interface.
29     Optionally uses msgpack for binary (de)serialization:
30     see https://msgpack.org/index.html
31
32     Published as a standalone module (and kept separate from the Xapp
33     framework classes) so these features can be used outside Xapps.
34     """
35
36     def __init__(self, use_fake_sdl=False):
37         """
38         init
39
40         Parameters
41         ----------
42         use_fake_sdl: bool (optional, default False)
43             if this is True, then use SDL's in-memory backend,
44             which is very useful for testing since it allows use
45             of SDL without a running SDL or Redis instance.
46             This can be used while developing an xapp and also
47             for monkeypatching during unit testing; e.g., the xapp
48             framework unit tests do this.
49         """
50         if use_fake_sdl:
51             self._sdl = SyncStorage(fake_db_backend="dict")
52         else:
53             self._sdl = SyncStorage()
54
55     def set(self, ns, key, value, usemsgpack=True):
56         """
57         Stores a key-value pair,
58         optionally serializing the value to bytes using msgpack.
59
60         TODO: discuss whether usemsgpack should *default* to True or
61         False here. This seems like a usage statistic question (that we
62         don't have enough data for yet). Are more uses for an xapp to
63         write/read their own data, or will more xapps end up reading data
64         written by some other thing? I think it's too early to know.
65
66         Parameters
67         ----------
68         ns: string
69             SDL namespace
70         key: string
71             SDL key
72         value:
73             Object or byte array to store.  See the `usemsgpack` parameter.
74         usemsgpack: boolean (optional, default is True)
75             Determines whether the value is serialized using msgpack before storing.
76             If usemsgpack is True, the msgpack function `packb` is invoked
77             on the value to yield a byte array that is then sent to SDL.
78             Stated differently, if usemsgpack is True, the value can be anything
79             that is serializable by msgpack.
80             If usemsgpack is False, the value must be bytes.
81         """
82         if usemsgpack:
83             value = msgpack.packb(value, use_bin_type=True)
84         self._sdl.set(ns, {key: value})
85
86     def set_if(self, ns, key, old_value, new_value, usemsgpack=True):
87         """
88         Conditionally modify the value of a key if the current value in data storage matches the
89         user's last known value.
90
91         Parameters
92         ----------
93         ns: string
94             SDL namespace
95         key: string
96             SDL key
97         old_value:
98             Lask known object or byte array.  See the `usemsgpack` parameter.
99         new_value:
100             Object or byte array to be written.  See the `usemsgpack` parameter.
101         usemsgpack: boolean (optional, default is True)
102             Determines whether the value is serialized using msgpack before storing.
103             If usemsgpack is True, the msgpack function `packb` is invoked
104             on the value to yield a byte array that is then sent to SDL.
105             Stated differently, if usemsgpack is True, the value can be anything
106             that is serializable by msgpack.
107             If usemsgpack is False, the value must be bytes.
108
109         Returns
110         -------
111         bool
112             True for successful modification, false if the user's last known data did not
113             match the current value in data storage.
114         """
115         if usemsgpack:
116             old_value = msgpack.packb(old_value, use_bin_type=True)
117             new_value = msgpack.packb(new_value, use_bin_type=True)
118         return self._sdl.set_if(ns, key, old_value, new_value)
119
120     def set_if_not_exists(self, ns, key, value, usemsgpack=True):
121         """
122         Write data to SDL storage if key does not exist.
123
124         Parameters
125         ----------
126         ns: string
127             SDL namespace
128         key: string
129             SDL key
130         value:
131             Object or byte array to store.  See the `usemsgpack` parameter.
132         usemsgpack: boolean (optional, default is True)
133             Determines whether the value is serialized using msgpack before storing.
134             If usemsgpack is True, the msgpack function `packb` is invoked
135             on the value to yield a byte array that is then sent to SDL.
136             Stated differently, if usemsgpack is True, the value can be anything
137             that is serializable by msgpack.
138             If usemsgpack is False, the value must be bytes.
139
140         Returns
141         -------
142         bool
143             True for successful modification, false if the user's last known data did not
144             match the current value in data storage.
145         """
146         if usemsgpack:
147             value = msgpack.packb(value, use_bin_type=True)
148         return self._sdl.set_if_not_exists(ns, key, value)
149
150     def get(self, ns, key, usemsgpack=True):
151         """
152         Gets the value for the specified namespace and key,
153         optionally deserializing stored bytes using msgpack.
154
155         Parameters
156         ----------
157         ns: string
158             SDL namespace
159         key: string
160             SDL key
161         usemsgpack: boolean (optional, default is True)
162             If usemsgpack is True, the byte array stored by SDL is deserialized
163             using msgpack to yield the original object that was stored.
164             If usemsgpack is False, the byte array stored by SDL is returned
165             without further processing.
166
167         Returns
168         -------
169         Value
170             See the usemsgpack parameter for an explanation of the returned value type.
171             Answers None if the key is not found.
172         """
173         result = None
174         ret_dict = self._sdl.get(ns, {key})
175         if key in ret_dict:
176             result = ret_dict[key]
177             if usemsgpack:
178                 result = msgpack.unpackb(result, raw=False)
179         return result
180
181     def find_keys(self, ns, prefix):
182         """
183         Find all keys matching search pattern under the namespace.
184
185         Parameters
186         ----------
187         ns: string
188             SDL namespace
189         prefix: string
190             Key search pattern
191
192         Returns
193         -------
194         keys: list
195             A list of found keys.
196         """
197         return self._sdl.find_keys(ns, f"{prefix}*")
198
199     def find_and_get(self, ns, prefix, usemsgpack=True):
200         """
201         Gets all key-value pairs in the specified namespace
202         with keys that start with the specified prefix,
203         optionally deserializing stored bytes using msgpack.
204
205         Parameters
206         ----------
207         ns: string
208            SDL namespace
209         prefix: string
210             the key prefix
211         usemsgpack: boolean (optional, default is True)
212             If usemsgpack is True, every byte array stored by SDL is deserialized
213             using msgpack to yield the original value that was stored.
214             If usemsgpack is False, every byte array stored by SDL is returned
215             without further processing.
216
217         Returns
218         -------
219         Dictionary of key-value pairs
220             Each key has the specified prefix.
221             See the usemsgpack parameter for an explanation of the returned value types.
222             Answers an empty dictionary if no keys matched the prefix.
223         """
224
225         # note: SDL "*" usage is inconsistent with real python regex, where it would be ".*"
226         ret_dict = self._sdl.find_and_get(ns, f"{prefix}*")
227         if usemsgpack:
228             ret_dict = {k: msgpack.unpackb(v, raw=False) for k, v in ret_dict.items()}
229         return ret_dict
230
231     def delete(self, ns, key):
232         """
233         Deletes the key-value pair with the specified key in the specified namespace.
234
235         Parameters
236         ----------
237         ns: string
238            SDL namespace
239         key: string
240             SDL key
241         """
242         self._sdl.remove(ns, {key})
243
244     def delete_if(self, ns, key, value, usemsgpack=True):
245         """
246         Conditionally remove data from SDL storage if the current data value matches the user's
247         last known value.
248
249         Parameters
250         ----------
251         ns: string
252             SDL namespace
253         key: string
254             SDL key
255         value:
256             Object or byte array to store.  See the `usemsgpack` parameter.
257         usemsgpack: boolean (optional, default is True)
258             Determines whether the value is serialized using msgpack before storing.
259             If usemsgpack is True, the msgpack function `packb` is invoked
260             on the value to yield a byte array that is then sent to SDL.
261             Stated differently, if usemsgpack is True, the value can be anything
262             that is serializable by msgpack.
263             If usemsgpack is False, the value must be bytes.
264
265         Returns
266         -------
267         bool
268             True if successful removal, false if the user's last known data did not match the
269             current value in data storage.
270         """
271         if usemsgpack:
272             value = msgpack.packb(value, use_bin_type=True)
273         return self._sdl.remove_if(ns, key, value)
274
275     def add_member(self, ns, group, member, usemsgpack=True):
276         """
277         Add new members to a SDL group under the namespace.
278
279         Parameters
280         ----------
281         ns: string
282             SDL namespace
283         group: string
284             group name
285         member:
286             member to be added
287         usemsgpack: boolean (optional, default is True)
288             Determines whether the member is serialized using msgpack before storing.
289             If usemsgpack is True, the msgpack function `packb` is invoked
290             on the member to yield a byte array that is then sent to SDL.
291             Stated differently, if usemsgpack is True, the member can be anything
292             that is serializable by msgpack.
293             If usemsgpack is False, the member must be bytes.
294         """
295         if usemsgpack:
296             member = msgpack.packb(member, use_bin_type=True)
297         self._sdl.add_member(ns, group, {member})
298
299     def remove_member(self, ns, group, member, usemsgpack=True):
300         """
301         Remove members from a SDL group.
302
303         Parameters
304         ----------
305         ns: string
306             SDL namespace
307         group: string
308             group name
309         member:
310             member to be removed
311         usemsgpack: boolean (optional, default is True)
312             Determines whether the member is serialized using msgpack before storing.
313             If usemsgpack is True, the msgpack function `packb` is invoked
314             on the member to yield a byte array that is then sent to SDL.
315             Stated differently, if usemsgpack is True, the member can be anything
316             that is serializable by msgpack.
317             If usemsgpack is False, the member must be bytes.
318         """
319         if usemsgpack:
320             member = msgpack.packb(member, use_bin_type=True)
321         self._sdl.remove_member(ns, group, {member})
322
323     def remove_group(self, ns, group):
324         """
325         Remove a SDL group along with its members.
326
327         Parameters
328         ----------
329         ns: string
330             SDL namespace
331         group: string
332             group name to remove
333         usemsgpack: boolean (optional, default is True)
334             Determines whether the member is serialized using msgpack before storing.
335             If usemsgpack is True, the msgpack function `packb` is invoked
336             on the member to yield a byte array that is then sent to SDL.
337             Stated differently, if usemsgpack is True, the member can be anything
338             that is serializable by msgpack.
339             If usemsgpack is False, the member must be bytes.
340         """
341         self._sdl.remove_group(ns, group)
342
343     def get_members(self, ns, group, usemsgpack=True):
344         """
345         Get all the members of a SDL group.
346
347         Parameters
348         ----------
349         ns: string
350             SDL namespace
351         group: string
352             group name to retrive
353         usemsgpack: boolean (optional, default is True)
354             Determines whether the member is serialized using msgpack before storing.
355             If usemsgpack is True, the msgpack function `packb` is invoked
356             on the member to yield a byte array that is then sent to SDL.
357             Stated differently, if usemsgpack is True, the member can be anything
358             that is serializable by msgpack.
359             If usemsgpack is False, the member must be bytes.
360
361         Returns
362         -------
363         Set[str] or Set[bytes]
364             A set of the members of the group.
365         None
366         """
367         ret_set = self._sdl.get_members(ns, group)
368         if usemsgpack:
369             ret_set = {msgpack.unpackb(m, raw=False) for m in ret_set}
370         return ret_set
371
372     def is_member(self, ns, group, member, usemsgpack=True):
373         """
374         Validate if a given member is in the SDL group.
375
376         Parameters
377         ----------
378         ns: string
379             SDL namespace
380         group: string
381             group name
382         member:
383             member to validate
384         usemsgpack: boolean (optional, default is True)
385             Determines whether the member is serialized using msgpack before storing.
386             If usemsgpack is True, the msgpack function `packb` is invoked
387             on the member to yield a byte array that is then sent to SDL.
388             Stated differently, if usemsgpack is True, the member can be anything
389             that is serializable by msgpack.
390             If usemsgpack is False, the member must be bytes.
391
392         Returns
393         -------
394         bool
395             True if member was in the group, false otherwise.
396         """
397         if usemsgpack:
398             member = msgpack.packb(member, use_bin_type=True)
399         return self._sdl.is_member(ns, group, member)
400
401     def group_size(self, ns, group):
402         """
403         Return the number of members in a group.
404         If the group does not exist, value 0 is returned.
405
406         Parameters
407         ----------
408         ns: string
409             SDL namespace
410         group: string
411             group name to retrive size
412         usemsgpack: boolean (optional, default is True)
413             Determines whether the member is serialized using msgpack before storing.
414             If usemsgpack is True, the msgpack function `packb` is invoked
415             on the member to yield a byte array that is then sent to SDL.
416             Stated differently, if usemsgpack is True, the member can be anything
417             that is serializable by msgpack.
418             If usemsgpack is False, the member must be bytes.
419
420         Returns
421         -------
422         int
423             Number of members in a group.
424         """
425         return self._sdl.group_size(ns, group)
426
427     def set_and_publish(self, ns, channel, event, key, value, usemsgpack=True):
428         """
429         Publish event to channel after writing data.
430
431         Parameters
432         ----------
433         ns: string
434             SDL namespace
435         channel: string
436             channel to publish event
437         event: string
438             published message
439         key: string
440             SDL key
441         value:
442             Object or byte array to store.  See the `usemsgpack` parameter.
443         usemsgpack: boolean (optional, default is True)
444             Determines whether the value is serialized using msgpack before storing.
445             If usemsgpack is True, the msgpack function `packb` is invoked
446             on the value to yield a byte array that is then sent to SDL.
447             Stated differently, if usemsgpack is True, the value can be anything
448             that is serializable by msgpack.
449             If usemsgpack is False, the value must be bytes.
450         """
451         if usemsgpack:
452             value = msgpack.packb(value, use_bin_type=True)
453         self._sdl.set_and_publish(ns, {channel: event}, {key: value})
454
455     def set_if_and_publish(self, ns, channel, event, key, old_value, new_value, usemsgpack=True):
456         """
457         Publish event to channel after conditionally modifying the value of a key if the
458         current value in data storage matches the user's last known value.
459
460         Parameters
461         ----------
462         ns: string
463             SDL namespace
464         channel: string
465             channel to publish event
466         event: string
467             published message
468         key: string
469             SDL key
470         old_value:
471             Lask known object or byte array.  See the `usemsgpack` parameter.
472         new_value:
473             Object or byte array to be written.  See the `usemsgpack` parameter.
474         usemsgpack: boolean (optional, default is True)
475             Determines whether the old_value & new_value is serialized using msgpack before storing.
476             If usemsgpack is True, the msgpack function `packb` is invoked
477             on the old_value & new_value to yield a byte array that is then sent to SDL.
478             Stated differently, if usemsgpack is True, the old_value & new_value can be anything
479             that is serializable by msgpack.
480             If usemsgpack is False, the old_value & new_value must be bytes.
481
482         Returns
483         -------
484         bool
485             True for successful modification, false if the user's last known data did not
486             match the current value in data storage.
487         """
488         if usemsgpack:
489             old_value = msgpack.packb(old_value, use_bin_type=True)
490             new_value = msgpack.packb(new_value, use_bin_type=True)
491         return self._sdl.set_if_and_publish(ns, {channel: event}, key, old_value, new_value)
492
493     def set_if_not_exists_and_publish(self, ns, channel, event, key, value, usemsgpack=True):
494         """
495         Publish event to channel after writing data to SDL storage if key does not exist.
496
497         Parameters
498         ----------
499         ns: string
500             SDL namespace
501         channel: string
502             channel to publish event
503         event: string
504             published message
505         key: string
506             SDL key
507         value:
508             Object or byte array to store.  See the `usemsgpack` parameter.
509         usemsgpack: boolean (optional, default is True)
510             Determines whether the value is serialized using msgpack before storing.
511             If usemsgpack is True, the msgpack function `packb` is invoked
512             on the value to yield a byte array that is then sent to SDL.
513             Stated differently, if usemsgpack is True, the value can be anything
514             that is serializable by msgpack.
515             If usemsgpack is False, the value must be bytes.
516
517         Returns
518         -------
519         bool
520             True if key didn't exist yet and set operation was executed, false if key already
521             existed and thus its value was left untouched.
522         """
523         if usemsgpack:
524             value = msgpack.packb(value, use_bin_type=True)
525         return self._sdl.set_if_not_exists_and_publish(ns, {channel: event}, key, value)
526
527     def remove_and_publish(self, ns, channel, event, key):
528         """
529         Publish event to channel after removing data.
530
531         Parameters
532         ----------
533         ns: string
534             SDL namespace
535         channel: string
536             channel to publish event
537         event: string
538             published message
539         key: string
540             SDL key
541         """
542         self._sdl.remove_and_publish(ns, {channel: event}, {key})
543
544     def remove_if_and_publish(self, ns, channel, event, key, value, usemsgpack=True):
545         """
546         Publish event to channel after removing key and its data from database if the
547         current data value is expected one.
548
549         Parameters
550         ----------
551         ns: string
552             SDL namespace
553         channel: string
554             channel to publish event
555         event: string
556             published message
557         key: string
558             SDL key
559         value:
560             Object or byte array to store.  See the `usemsgpack` parameter.
561         usemsgpack: boolean (optional, default is True)
562             Determines whether the value is serialized using msgpack before storing.
563             If usemsgpack is True, the msgpack function `packb` is invoked
564             on the value to yield a byte array that is then sent to SDL.
565             Stated differently, if usemsgpack is True, the value can be anything
566             that is serializable by msgpack.
567             If usemsgpack is False, the value must be bytes.
568
569         Returns
570         -------
571         bool
572             True if successful removal, false if the user's last known data did not match the
573             current value in data storage.
574         """
575         if usemsgpack:
576             value = msgpack.packb(value, use_bin_type=True)
577         return self._sdl.remove_if_and_publish(ns, {channel: event}, key, value)
578
579     def remove_all_and_publish(self, ns, channel, event):
580         """
581         Publish event to channel after removing all keys under the namespace.
582
583         Parameters
584         ----------
585         ns: string
586             SDL namespace
587         channel: string
588             channel to publish event
589         event: string
590             published message
591         """
592         self._sdl.remove_all_and_publish(ns, {channel: event})
593
594     def subscribe_channel(self, ns, cb, channel):
595         """
596         Subscribes the client to the specified channels.
597
598         Parameters
599         ----------
600         ns: string
601             SDL namespace
602         cb:
603             A function that is called when an event on channel is received.
604         channel: string
605             channel to subscribe
606         """
607         self._sdl.subscribe_channel(ns, cb, {channel})
608
609     def unsubscribe_channel(self, ns, channel):
610         """
611         unsubscribe_channel removes subscription from one or several channels.
612
613         Parameters
614         ----------
615         ns: string
616             SDL namespace
617         channel: string
618             channel to unsubscribe
619         """
620         self._sdl.unsubscribe_channel(ns, {channel})
621
622     def start_event_listener(self):
623         """
624         start_event_listener creates an event loop in a separate thread for handling
625         events from subscriptions. The registered callback function will be called
626         when an event is received.
627         """
628         self._sdl.start_event_listener()
629
630     def handle_events(self):
631         """
632         handle_events is a non-blocking function that returns a tuple containing channel
633         name and a list of message(s) received from an event. The registered callback
634         function will still be called when an event is received.
635
636         This function is called if SDL user decides to handle notifications in its own
637         event loop. Calling this function after start_event_listener raises an exception.
638         If there are no notifications, these returns None.
639
640         Returns
641         -------
642         Tuple:
643             (channel: str, message: list of str)
644         """
645         return self._sdl.handle_events()
646
647     def healthcheck(self):
648         """
649         Checks if the sdl connection is healthy.
650
651         Returns
652         -------
653         bool
654         """
655         return self._sdl.is_active()