1# encoding=utf-8 2 3import collections 4import itertools 5 6from six import iteritems 7 8from .exceptions import ConversionError, ModelConversionError, ValidationError 9from .datastructures import OrderedDict 10 11try: 12 basestring #PY2 13except NameError: 14 basestring = str #PY3 15 16def _list_or_string(lors): 17 if lors is None: 18 return [] 19 if isinstance(lors, basestring): 20 return [lors] 21 return list(lors) 22 23try: 24 unicode #PY2 25except: 26 import codecs 27 unicode = str #PY3 28 29### 30# Transform Loops 31### 32 33def import_loop(cls, instance_or_dict, field_converter, context=None, 34 partial=False, strict=False, mapping=None): 35 """ 36 The import loop is designed to take untrusted data and convert it into the 37 native types, as described in ``cls``. It does this by calling 38 ``field_converter`` on every field. 39 40 Errors are aggregated and returned by throwing a ``ModelConversionError``. 41 42 :param cls: 43 The class for the model. 44 :param instance_or_dict: 45 A dict of data to be converted into types according to ``cls``. 46 :param field_convert: 47 This function is applied to every field found in ``instance_or_dict``. 48 :param context: 49 A ``dict``-like structure that may contain already validated data. 50 :param partial: 51 Allow partial data to validate; useful for PATCH requests. 52 Essentially drops the ``required=True`` arguments from field 53 definitions. Default: False 54 :param strict: 55 Complain about unrecognized keys. Default: False 56 """ 57 is_dict = isinstance(instance_or_dict, dict) 58 is_cls = isinstance(instance_or_dict, cls) 59 if not is_cls and not is_dict: 60 error_msg = 'Model conversion requires a model or dict' 61 raise ModelConversionError(error_msg) 62 if mapping is None: 63 mapping = {} 64 data = dict(context) if context is not None else {} 65 errors = {} 66 67 # Determine all acceptable field input names 68 all_fields = set(cls._fields) ^ set(cls._serializables) 69 for field_name, field, in iteritems(cls._fields): 70 if hasattr(field, 'serialized_name'): 71 all_fields.add(field.serialized_name) 72 if hasattr(field, 'deserialize_from'): 73 all_fields.update(set(_list_or_string(field.deserialize_from))) 74 if field_name in mapping: 75 all_fields.update(set(_list_or_string(mapping[field_name]))) 76 77 # Check for rogues if strict is set 78 rogue_fields = set(instance_or_dict) - set(all_fields) 79 if strict and len(rogue_fields) > 0: 80 for field in rogue_fields: 81 errors[field] = 'Rogue field' 82 83 for field_name, field in iteritems(cls._fields): 84 serialized_field_name = field.serialized_name or field_name 85 86 trial_keys = _list_or_string(field.deserialize_from) 87 trial_keys.extend(mapping.get(field_name, [])) 88 trial_keys.extend([serialized_field_name, field_name]) 89 90 raw_value = None 91 for key in trial_keys: 92 if key and key in instance_or_dict: 93 raw_value = instance_or_dict[key] 94 if raw_value is None: 95 if field_name in data: 96 continue 97 raw_value = field.default 98 99 try: 100 if raw_value is None: 101 if field.required and not partial: 102 errors[serialized_field_name] = [field.messages['required']] 103 else: 104 try: 105 mapping_by_model = mapping.get('model_mapping', {}) 106 model_mapping = mapping_by_model.get(field_name, {}) 107 raw_value = field_converter(field, raw_value, mapping=model_mapping) 108 except Exception: 109 raw_value = field_converter(field, raw_value) 110 111 data[field_name] = raw_value 112 113 except ConversionError as exc: 114 errors[serialized_field_name] = exc.messages 115 except ValidationError as exc: 116 errors[serialized_field_name] = exc.messages 117 118 if errors: 119 raise ModelConversionError(errors, data) 120 121 return data 122 123 124def export_loop(cls, instance_or_dict, field_converter, 125 role=None, raise_error_on_role=False, print_none=False): 126 """ 127 The export_loop function is intended to be a general loop definition that 128 can be used for any form of data shaping, such as application of roles or 129 how a field is transformed. 130 131 :param cls: 132 The model definition. 133 :param instance_or_dict: 134 The structure where fields from cls are mapped to values. The only 135 expectionation for this structure is that it implements a ``dict`` 136 interface. 137 :param field_converter: 138 This function is applied to every field found in ``instance_or_dict``. 139 :param role: 140 The role used to determine if fields should be left out of the 141 transformation. 142 :param raise_error_on_role: 143 This parameter enforces strict behavior which requires substructures 144 to have the same role definition as their parent structures. 145 :param print_none: 146 This function overrides ``serialize_when_none`` values found either on 147 ``cls`` or an instance. 148 """ 149 data = {} 150 151 # Translate `role` into `gottago` function 152 gottago = wholelist() 153 if hasattr(cls, '_options') and role in cls._options.roles: 154 gottago = cls._options.roles[role] 155 elif role and raise_error_on_role: 156 error_msg = u'%s Model has no role "%s"' 157 raise ValueError(error_msg % (cls.__name__, role)) 158 else: 159 gottago = cls._options.roles.get("default", gottago) 160 161 fields_order = (getattr(cls._options, 'fields_order', None) 162 if hasattr(cls, '_options') else None) 163 164 for field_name, field, value in atoms(cls, instance_or_dict): 165 serialized_name = field.serialized_name or field_name 166 167 # Skipping this field was requested 168 if gottago(field_name, value): 169 continue 170 171 # Value found, apply transformation and store it 172 elif value is not None: 173 if hasattr(field, 'export_loop'): 174 shaped = field.export_loop(value, field_converter, 175 role=role, 176 print_none=print_none) 177 else: 178 shaped = field_converter(field, value) 179 180 # Print if we want none or found a value 181 if shaped is None and allow_none(cls, field): 182 data[serialized_name] = shaped 183 elif shaped is not None: 184 data[serialized_name] = shaped 185 elif print_none: 186 data[serialized_name] = shaped 187 188 # Store None if reqeusted 189 elif value is None and allow_none(cls, field): 190 data[serialized_name] = value 191 elif print_none: 192 data[serialized_name] = value 193 194 # Return data if the list contains anything 195 if len(data) > 0: 196 if fields_order: 197 return sort_dict(data, fields_order) 198 return data 199 elif print_none: 200 return data 201 202 203def sort_dict(dct, based_on): 204 """ 205 Sorts provided dictionary based on order of keys provided in ``based_on`` 206 list. 207 208 Order is not guarantied in case if ``dct`` has keys that are not present 209 in ``based_on`` 210 211 :param dct: 212 Dictionary to be sorted. 213 :param based_on: 214 List of keys in order that resulting dictionary should have. 215 :return: 216 OrderedDict with keys in the same order as provided ``based_on``. 217 """ 218 return OrderedDict( 219 sorted( 220 dct.items(), 221 key=lambda el: based_on.index(el[0] if el[0] in based_on else -1)) 222 ) 223 224 225def atoms(cls, instance_or_dict): 226 """ 227 Iterator for the atomic components of a model definition and relevant data 228 that creates a threeple of the field's name, the instance of it's type, and 229 it's value. 230 231 :param cls: 232 The model definition. 233 :param instance_or_dict: 234 The structure where fields from cls are mapped to values. The only 235 expectionation for this structure is that it implements a ``dict`` 236 interface. 237 """ 238 all_fields = itertools.chain(iteritems(cls._fields), 239 iteritems(cls._serializables)) 240 241 return ((field_name, field, instance_or_dict[field_name]) 242 for field_name, field in all_fields) 243 244 245def allow_none(cls, field): 246 """ 247 This function inspects a model and a field for a setting either at the 248 model or field level for the ``serialize_when_none`` setting. 249 250 The setting defaults to the value of the class. A field can override the 251 class setting with it's own ``serialize_when_none`` setting. 252 253 :param cls: 254 The model definition. 255 :param field: 256 The field in question. 257 """ 258 allowed = cls._options.serialize_when_none 259 if field.serialize_when_none is not None: 260 allowed = field.serialize_when_none 261 return allowed 262 263 264### 265# Field Filtering 266### 267 268class Role(collections.Set): 269 270 """ 271 A ``Role`` object can be used to filter specific fields against a sequence. 272 273 The ``Role`` is two things: a set of names and a function. The function 274 describes how filter taking a field name as input and then returning either 275 ``True`` or ``False``, indicating that field should or should not be 276 skipped. 277 278 A ``Role`` can be operated on as a ``Set`` object representing the fields 279 is has an opinion on. When Roles are combined with other roles, the 280 filtering behavior of the first role is used. 281 """ 282 283 def __init__(self, function, fields): 284 self.function = function 285 self.fields = set(fields) 286 287 def _from_iterable(self, iterable): 288 return Role(self.function, iterable) 289 290 def __contains__(self, value): 291 return value in self.fields 292 293 def __iter__(self): 294 return iter(self.fields) 295 296 def __len__(self): 297 return len(self.fields) 298 299 def __eq__(self, other): 300 print(dir(self.function)) 301 return (self.function.__name__ == other.function.__name__ and 302 self.fields == other.fields) 303 304 def __str__(self): 305 return '%s(%s)' % (self.function.__name__, 306 ', '.join("'%s'" % f for f in self.fields)) 307 308 def __repr__(self): 309 return '<Role %s>' % str(self) 310 311 # edit role fields 312 def __add__(self, other): 313 fields = self.fields.union(other) 314 return self._from_iterable(fields) 315 316 def __sub__(self, other): 317 fields = self.fields.difference(other) 318 return self._from_iterable(fields) 319 320 # apply role to field 321 def __call__(self, name, value): 322 return self.function(name, value, self.fields) 323 324 # static filter functions 325 @staticmethod 326 def wholelist(name, value, seq): 327 """ 328 Accepts a field name, value, and a field list. This functions 329 implements acceptance of all fields by never requesting a field be 330 skipped, thus returns False for all input. 331 332 :param name: 333 The field name to inspect. 334 :param value: 335 The field's value. 336 :param seq: 337 The list of fields associated with the ``Role``. 338 """ 339 return False 340 341 @staticmethod 342 def whitelist(name, value, seq): 343 """ 344 Implements the behavior of a whitelist by requesting a field be skipped 345 whenever it's name is not in the list of fields. 346 347 :param name: 348 The field name to inspect. 349 :param value: 350 The field's value. 351 :param seq: 352 The list of fields associated with the ``Role``. 353 """ 354 355 if seq is not None and len(seq) > 0: 356 return name not in seq 357 return True 358 359 @staticmethod 360 def blacklist(name, value, seq): 361 """ 362 Implements the behavior of a blacklist by requesting a field be skipped 363 whenever it's name is found in the list of fields. 364 365 :param k: 366 The field name to inspect. 367 :param v: 368 The field's value. 369 :param seq: 370 The list of fields associated with the ``Role``. 371 """ 372 if seq is not None and len(seq) > 0: 373 return name in seq 374 return False 375 376 377def wholelist(*field_list): 378 """ 379 Returns a function that evicts nothing. Exists mainly to be an explicit 380 allowance of all fields instead of a using an empty blacklist. 381 """ 382 return Role(Role.wholelist, field_list) 383 384 385def whitelist(*field_list): 386 """ 387 Returns a function that operates as a whitelist for the provided list of 388 fields. 389 390 A whitelist is a list of fields explicitly named that are allowed. 391 """ 392 return Role(Role.whitelist, field_list) 393 394 395def blacklist(*field_list): 396 """ 397 Returns a function that operates as a blacklist for the provided list of 398 fields. 399 400 A blacklist is a list of fields explicitly named that are not allowed. 401 """ 402 return Role(Role.blacklist, field_list) 403 404 405### 406# Import and export functions 407### 408 409 410def convert(cls, instance_or_dict, context=None, partial=True, strict=False, 411 mapping=None): 412 def field_converter(field, value, mapping=None): 413 try: 414 return field.to_native(value, mapping=mapping) 415 except Exception: 416 return field.to_native(value) 417# field_converter = lambda field, value: field.to_native(value) 418 data = import_loop(cls, instance_or_dict, field_converter, context=context, 419 partial=partial, strict=strict, mapping=mapping) 420 return data 421 422 423def to_native(cls, instance_or_dict, role=None, raise_error_on_role=True, 424 context=None): 425 field_converter = lambda field, value: field.to_native(value, 426 context=context) 427 data = export_loop(cls, instance_or_dict, field_converter, 428 role=role, raise_error_on_role=raise_error_on_role) 429 return data 430 431 432def to_primitive(cls, instance_or_dict, role=None, raise_error_on_role=True, 433 context=None): 434 """ 435 Implements serialization as a mechanism to convert ``Model`` instances into 436 dictionaries keyed by field_names with the converted data as the values. 437 438 The conversion is done by calling ``to_primitive`` on both model and field 439 instances. 440 441 :param cls: 442 The model definition. 443 :param instance_or_dict: 444 The structure where fields from cls are mapped to values. The only 445 expectionation for this structure is that it implements a ``dict`` 446 interface. 447 :param role: 448 The role used to determine if fields should be left out of the 449 transformation. 450 :param raise_error_on_role: 451 This parameter enforces strict behavior which requires substructures 452 to have the same role definition as their parent structures. 453 """ 454 field_converter = lambda field, value: field.to_primitive(value, 455 context=context) 456 data = export_loop(cls, instance_or_dict, field_converter, 457 role=role, raise_error_on_role=raise_error_on_role) 458 return data 459 460 461def serialize(cls, instance_or_dict, role=None, raise_error_on_role=True, 462 context=None): 463 return to_primitive(cls, instance_or_dict, role, raise_error_on_role, 464 context) 465 466 467EMPTY_LIST = "[]" 468EMPTY_DICT = "{}" 469 470 471def expand(data, context=None): 472 """ 473 Expands a flattened structure into it's corresponding layers. Essentially, 474 it is the counterpart to ``flatten_to_dict``. 475 476 :param data: 477 The data to expand. 478 :param context: 479 Existing expanded data that this function use for output 480 """ 481 expanded_dict = {} 482 483 if context is None: 484 context = expanded_dict 485 486 for key, value in iteritems(data): 487 try: 488 key, remaining = key.split(".", 1) 489 except ValueError: 490 if not (value in (EMPTY_DICT, EMPTY_LIST) and key in expanded_dict): 491 expanded_dict[key] = value 492 else: 493 current_context = context.setdefault(key, {}) 494 if current_context in (EMPTY_DICT, EMPTY_LIST): 495 current_context = {} 496 context[key] = current_context 497 498 current_context.update(expand({remaining: value}, current_context)) 499 return expanded_dict 500 501 502def flatten_to_dict(instance_or_dict, prefix=None, ignore_none=True): 503 """ 504 Flattens an iterable structure into a single layer dictionary. 505 506 For example: 507 508 { 509 's': 'jms was hrrr', 510 'l': ['jms was here', 'here', 'and here'] 511 } 512 513 becomes 514 515 { 516 's': 'jms was hrrr', 517 u'l.1': 'here', 518 u'l.0': 'jms was here', 519 u'l.2': 'and here' 520 } 521 522 :param instance_or_dict: 523 The structure where fields from cls are mapped to values. The only 524 expectionation for this structure is that it implements a ``dict`` 525 interface. 526 :param ignore_none: 527 This ignores any ``serialize_when_none`` settings and forces the empty 528 fields to be printed as part of the flattening. 529 Default: True 530 :param prefix: 531 This puts a prefix in front of the field names during flattening. 532 Default: None 533 """ 534 if isinstance(instance_or_dict, dict): 535 iterator = iteritems(instance_or_dict) 536 # if hasattr(instance_or_dict, "iteritems"): 537 # iterator = instance_or_dict.iteritems() 538 else: 539 iterator = enumerate(instance_or_dict) 540 541 flat_dict = {} 542 for key, value in iterator: 543 if prefix: 544 key = ".".join(map(unicode, (prefix, key))) 545 546 if value == []: 547 value = EMPTY_LIST 548 elif value == {}: 549 value = EMPTY_DICT 550 551 if isinstance(value, (dict, list)): 552 flat_dict.update(flatten_to_dict(value, prefix=key)) 553 elif value is not None: 554 flat_dict[key] = value 555 elif not ignore_none: 556 flat_dict[key] = None 557 558 return flat_dict 559 560 561def flatten(cls, instance_or_dict, role=None, raise_error_on_role=True, 562 ignore_none=True, prefix=None, context=None): 563 """ 564 Produces a flat dictionary representation of the model. Flat, in this 565 context, means there is only one level to the dictionary. Multiple layers 566 are represented by the structure of the key. 567 568 Example: 569 570 >>> class Foo(Model): 571 ... s = StringType() 572 ... l = ListType(StringType) 573 574 >>> f = Foo() 575 >>> f.s = 'string' 576 >>> f.l = ['jms', 'was here', 'and here'] 577 578 >>> flatten(Foo, f) 579 {'s': 'string', u'l.1': 'jms', u'l.0': 'was here', u'l.2': 'and here'} 580 581 :param cls: 582 The model definition. 583 :param instance_or_dict: 584 The structure where fields from cls are mapped to values. The only 585 expectionation for this structure is that it implements a ``dict`` 586 interface. 587 :param role: 588 The role used to determine if fields should be left out of the 589 transformation. 590 :param raise_error_on_role: 591 This parameter enforces strict behavior which requires substructures 592 to have the same role definition as their parent structures. 593 :param ignore_none: 594 This ignores any ``serialize_when_none`` settings and forces the empty 595 fields to be printed as part of the flattening. 596 Default: True 597 :param prefix: 598 This puts a prefix in front of the field names during flattening. 599 Default: None 600 """ 601 field_converter = lambda field, value: field.to_primitive(value, 602 context=context) 603 604 data = export_loop(cls, instance_or_dict, field_converter, 605 role=role, print_none=True) 606 607 flattened = flatten_to_dict(data, prefix=prefix, ignore_none=ignore_none) 608 609 return flattened 610