1""" 2This module's purpose is to enable us to present internals of objects 3in well-defined way to operator. To do this we can define "views" 4on some objects. View is a definition of how to present object 5and relations to other objects which also have their views defined. 6 7By using views we can avoid making all interesting internal values 8public. They will stay private and only "view" will access them 9(think friend-class from C++) 10""" 11import logging 12 13from ryu.services.protocols.bgp.operator.views import fields 14 15LOG = logging.getLogger('bgpspeaker.operator.views.base') 16 17 18class RdyToFlattenCollection(object): 19 pass 20 21 22class RdyToFlattenList(list, RdyToFlattenCollection): 23 pass 24 25 26class RdyToFlattenDict(dict, RdyToFlattenCollection): 27 pass 28 29 30class OperatorAbstractView(object): 31 """Abstract base class for operator views. It isn't meant to be 32 instantiated. 33 """ 34 35 def __init__(self, obj, filter_func=None): 36 """Init 37 38 :param obj: data model for view. In other words object we 39 are creating view for. In case of ListView it should be 40 a list and in case of DictView it should be a dict. 41 :param filter_func: function to filter models 42 """ 43 self._filter_func = filter_func 44 self._fields = self._collect_fields() 45 self._obj = obj 46 47 @classmethod 48 def _collect_fields(cls): 49 names = [attr for attr in dir(cls) 50 if isinstance(getattr(cls, attr), fields.Field)] 51 return dict([(name, getattr(cls, name)) for name in names]) 52 53 def combine_related(self, field_name): 54 """Combines related views. In case of DetailView it just returns 55 one-element list containing related view wrapped in 56 CombinedViewsWrapper. 57 58 In case of ListView and DictView it returns a list of related views 59 for every element of model collection also wrapped 60 in CombinedViewsWrapper. 61 62 :param field_name: field name of related view 63 :returns: vectorized form of related views. You can access them 64 as if you had only one view and you will receive flattened list 65 of responses from related views. Look at docstring of 66 CombinedViewsWrapper 67 """ 68 raise NotImplementedError() 69 70 def c_rel(self, *args, **kwargs): 71 """Shortcut for combine_related. Look above 72 """ 73 return self.combine_related(*args, **kwargs) 74 75 def get_field(self, field_name): 76 """Get value of data field. 77 78 :return: value of data-field of this view 79 """ 80 raise NotImplementedError() 81 82 def encode(self): 83 """Representation of view which is using only python standard types. 84 85 :return: dict representation of this views data. However it 86 doesn't have to be a dict. In case of ListView it would 87 return a list. It should return wrapped types 88 for list - RdyToFlattenList, for dict - RdyToFlattenDict 89 """ 90 raise NotImplementedError() 91 92 @property 93 def model(self): 94 """Getter for data model being presented by this view. Every view is 95 associated with some data model. 96 97 :return: underlaying data of this view 98 """ 99 raise NotImplementedError() 100 101 def apply_filter(self, filter_func): 102 """Sets filter function to apply on model 103 104 :param filter_func: function which takes the model and returns it 105 filtered 106 """ 107 self._filter_func = filter_func 108 109 def clear_filter(self): 110 self._filter_func = None 111 112 113class OperatorDetailView(OperatorAbstractView): 114 def combine_related(self, field_name): 115 f = self._fields[field_name] 116 return CombinedViewsWrapper([f.retrieve_and_wrap(self._obj)]) 117 118 def get_field(self, field_name): 119 f = self._fields[field_name] 120 return f.get(self._obj) 121 122 def encode(self): 123 encoded = {} 124 for field_name, field in self._fields.items(): 125 if isinstance(field, fields.DataField): 126 encoded[field_name] = field.get(self._obj) 127 return encoded 128 129 def rel(self, field_name): 130 f = self._fields[field_name] 131 return f.retrieve_and_wrap(self._obj) 132 133 @property 134 def model(self): 135 return self._obj 136 137 138class OperatorListView(OperatorAbstractView): 139 def __init__(self, obj, filter_func=None): 140 assert isinstance(obj, list) 141 obj = RdyToFlattenList(obj) 142 super(OperatorListView, self).__init__(obj, filter_func) 143 144 def combine_related(self, field_name): 145 f = self._fields[field_name] 146 return CombinedViewsWrapper(RdyToFlattenList( 147 [f.retrieve_and_wrap(obj) for obj in self.model] 148 )) 149 150 def get_field(self, field_name): 151 f = self._fields[field_name] 152 return RdyToFlattenList([f.get(obj) for obj in self.model]) 153 154 def encode(self): 155 encoded_list = [] 156 for obj in self.model: 157 encoded_item = {} 158 for field_name, field in self._fields.items(): 159 if isinstance(field, fields.DataField): 160 encoded_item[field_name] = field.get(obj) 161 encoded_list.append(encoded_item) 162 return RdyToFlattenList(encoded_list) 163 164 @property 165 def model(self): 166 if self._filter_func is not None: 167 return RdyToFlattenList(filter(self._filter_func, self._obj)) 168 else: 169 return self._obj 170 171 172class OperatorDictView(OperatorAbstractView): 173 def __init__(self, obj, filter_func=None): 174 assert isinstance(obj, dict) 175 obj = RdyToFlattenDict(obj) 176 super(OperatorDictView, self).__init__(obj, filter_func) 177 178 def combine_related(self, field_name): 179 f = self._fields[field_name] 180 return CombinedViewsWrapper(RdyToFlattenList( 181 [f.retrieve_and_wrap(obj) for obj in self.model.values()]) 182 ) 183 184 def get_field(self, field_name): 185 f = self._fields[field_name] 186 dict_to_flatten = {} 187 for key, obj in self.model.items(): 188 dict_to_flatten[key] = f.get(obj) 189 return RdyToFlattenDict(dict_to_flatten) 190 191 def encode(self): 192 outer_dict_to_flatten = {} 193 for key, obj in self.model.items(): 194 inner_dict_to_flatten = {} 195 for field_name, field in self._fields.items(): 196 if isinstance(field, fields.DataField): 197 inner_dict_to_flatten[field_name] = field.get(obj) 198 outer_dict_to_flatten[key] = inner_dict_to_flatten 199 return RdyToFlattenDict(outer_dict_to_flatten) 200 201 @property 202 def model(self): 203 if self._filter_func is not None: 204 new_model = RdyToFlattenDict() 205 for k, v in self._obj.items(): 206 if self._filter_func(k, v): 207 new_model[k] = v 208 return new_model 209 else: 210 return self._obj 211 212 213class CombinedViewsWrapper(RdyToFlattenList): 214 """List-like wrapper for views. It provides same interface as any other 215 views but enables as to access all views in bulk. 216 It wraps and return responses from all views as a list. Be aware that 217 in case of DictViews wrapped in CombinedViewsWrapper you loose 218 information about dict keys. 219 """ 220 221 def __init__(self, obj): 222 super(CombinedViewsWrapper, self).__init__(obj) 223 self._obj = obj 224 225 def combine_related(self, field_name): 226 return CombinedViewsWrapper( 227 list(_flatten( 228 [obj.combine_related(field_name) for obj in self._obj] 229 )) 230 ) 231 232 def c_rel(self, *args, **kwargs): 233 return self.combine_related(*args, **kwargs) 234 235 def encode(self): 236 return list(_flatten([obj.encode() for obj in self._obj])) 237 238 def get_field(self, field_name): 239 return list(_flatten([obj.get_field(field_name) for obj in self._obj])) 240 241 @property 242 def model(self): 243 return list(_flatten([obj.model for obj in self._obj])) 244 245 def apply_filter(self, filter_func): 246 for obj in self._obj: 247 obj.apply_filter(filter_func) 248 249 def clear_filter(self): 250 for obj in self._obj: 251 obj.clear_filter() 252 253 254def _flatten(l, max_level=10): 255 """Generator function going deep in tree-like structures 256 (i.e. dicts in dicts or lists in lists etc.) and returning all elements as 257 a flat list. It's flattening only lists and dicts which are subclasses of 258 RdyToFlattenCollection. Regular lists and dicts are treated as a 259 single items. 260 261 :param l: some iterable to be flattened 262 :return: flattened iterator 263 """ 264 if max_level >= 0: 265 _iter = l.values() if isinstance(l, dict) else l 266 for el in _iter: 267 if isinstance(el, RdyToFlattenCollection): 268 for sub in _flatten(el, max_level=max_level - 1): 269 yield sub 270 else: 271 yield el 272 else: 273 yield l 274 275 276def _create_collection_view(detail_view_class, name, encode=None, 277 view_class=None): 278 assert issubclass(detail_view_class, OperatorDetailView) 279 class_fields = detail_view_class._collect_fields() 280 if encode is not None: 281 class_fields.update({'encode': encode}) 282 return type(name, (view_class,), class_fields) 283 284 285# function creating ListView from DetailView 286def create_dict_view_class(detail_view_class, name): 287 encode = None 288 if 'encode' in dir(detail_view_class): 289 def encode(self): 290 dict_to_flatten = {} 291 for key, obj in self.model.items(): 292 dict_to_flatten[key] = detail_view_class(obj).encode() 293 return RdyToFlattenDict(dict_to_flatten) 294 295 return _create_collection_view( 296 detail_view_class, name, encode, OperatorDictView 297 ) 298 299 300# function creating DictView from DetailView 301def create_list_view_class(detail_view_class, name): 302 encode = None 303 if 'encode' in dir(detail_view_class): 304 def encode(self): 305 return RdyToFlattenList([detail_view_class(obj).encode() 306 for obj in self.model]) 307 308 return _create_collection_view( 309 detail_view_class, name, encode, OperatorListView 310 ) 311