Completed
Push — develop ( ce1198...1cbae8 )
by Kale
9s
created

ListField.box()   C

Complexity

Conditions 8

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 1
Metric Value
cc 8
c 3
b 0
f 1
dl 0
loc 15
rs 6.6666
1
# -*- coding: utf-8 -*-
2
"""
3
This module provides serializable, validatable, type-enforcing domain objects and data
4
transfer objects. It has many of the same motivations as the python
5
`Marshmallow <https://marshmallow.readthedocs.io/en/latest/why.html>`_ package. It is most
6
similar to `Schematics <https://schematics.readthedocs.io/>`_.
7
8
========
9
Tutorial
10
========
11
12
Chapter 1: Entity and Field Basics
13
----------------------------------
14
15
    >>> class Color(Enum):
16
    ...     blue = 0
17
    ...     black = 1
18
    ...     red = 2
19
    >>> class Car(Entity):
20
    ...     weight = NumberField(required=False)
21
    ...     wheels = IntField(default=4, validation=lambda x: 3 <= x <= 4)
22
    ...     color = EnumField(Color)
23
24
    >>> # create a new car object
25
    >>> car = Car(color=Color.blue, weight=4242.46)
26
    >>> car
27
    Car(weight=4242.46, color=0)
28
29
    >>> # it has 4 wheels, all by default
30
    >>> car.wheels
31
    4
32
33
    >>> # but a car can't have 5 wheels!
34
    >>> #  the `validation=` field is a simple callable that returns a
35
    >>> #  boolean based on validity
36
    >>> car.wheels = 5
37
    Traceback (most recent call last):
38
    ValidationError: Invalid value 5 for wheels
39
40
    >>> # we can call .dump() on car, and just get back a standard
41
    >>> #  python dict actually, it's an ordereddict to match attribute
42
    >>> #  declaration order
43
    >>> type(car.dump())
44
    <class '...OrderedDict'>
45
    >>> car.dump()
46
    OrderedDict([('weight', 4242.46), ('wheels', 4), ('color', 0)])
47
48
    >>> # and json too (note the order!)
49
    >>> car.json()
50
    '{"weight": 4242.46, "wheels": 4, "color": 0}'
51
52
    >>> # green cars aren't allowed
53
    >>> car.color = "green"
54
    Traceback (most recent call last):
55
    ValidationError: 'green' is not a valid Color
56
57
    >>> # but black cars are!
58
    >>> car.color = "black"
59
    >>> car.color
60
    <Color.black: 1>
61
62
    >>> # car.color really is an enum, promise
63
    >>> type(car.color)
64
    <enum 'Color'>
65
66
    >>> # enum assignment can be with any of (and preferentially)
67
    >>> #   (1) an enum literal,
68
    >>> #   (2) a valid enum value, or
69
    >>> #   (3) a valid enum name
70
    >>> car.color = Color.blue; car.color.value
71
    0
72
    >>> car.color = 1; car.color.name
73
    'black'
74
75
    >>> # let's do a round-trip marshalling of this thing
76
    >>> same_car = Car.from_json(car.json())  # or equally Car.from_json(json.dumps(car.dump()))
77
    >>> same_car == car
78
    True
79
80
    >>> # actually, they're two different instances
81
    >>> same_car is not car
82
    True
83
84
    >>> # this works too
85
    >>> cloned_car = Car(**car.dump())
86
    >>> cloned_car == car
87
    True
88
89
    >>> # while we're at it, these are all equivalent too
90
    >>> car == Car.from_objects(car)
91
    True
92
    >>> car == Car.from_objects({"weight": 4242.46, "wheels": 4, "color": 1})
93
    True
94
    >>> car == Car.from_json('{"weight": 4242.46, "color": 1}')
95
    True
96
97
    >>> # .from_objects() even lets you stack and combine objects
98
    >>> class DumbClass:
99
    ...     color = 0
100
    ...     wheels = 3
101
    >>> Car.from_objects(DumbClass(), dict(weight=2222, color=1))
102
    Car(weight=2222, wheels=3, color=0)
103
    >>> # and also pass kwargs that override properties pulled
104
    >>> #  off any objects
105
    >>> Car.from_objects(DumbClass(), {'weight': 2222, 'color': 1}, color=2, weight=33)
106
    Car(weight=33, wheels=3, color=2)
107
108
109
Chapter 2: Entity and Field Composition
110
---------------------------------------
111
112
    >>> # now let's get fancy
113
    >>> # a ComposableField "nests" another valid Entity
114
    >>> # a ListField's first argument is a "generic" type,
115
    >>> #   which can be a valid Entity, any python primitive
116
    >>> #   type, or a list of Entities/types
117
    >>> class Fleet(Entity):
118
    ...     boss_car = ComposableField(Car)
119
    ...     cars = ListField(Car)
120
121
    >>> # here's our fleet of company cars
122
    >>> company_fleet = Fleet(boss_car=Car(color='red'), cars=[car, same_car, cloned_car])
123
    >>> company_fleet.pretty_json()  #doctest: +SKIP
124
    {
125
      "boss_car": {
126
        "wheels": 4
127
        "color": 2,
128
      },
129
      "cars": [
130
        {
131
          "weight": 4242.46,
132
          "wheels": 4
133
          "color": 1,
134
        },
135
        {
136
          "weight": 4242.46,
137
          "wheels": 4
138
          "color": 1,
139
        },
140
        {
141
          "weight": 4242.46,
142
          "wheels": 4
143
          "color": 1,
144
        }
145
      ]
146
    }
147
148
    >>> # the boss' car is red of course (and it's still an Enum)
149
    >>> company_fleet.boss_car.color.name
150
    'red'
151
152
    >>> # and there are three cars left for the employees
153
    >>> len(company_fleet.cars)
154
    3
155
156
157
Chapter 3: Immutability
158
-----------------------
159
160
    >>> class ImmutableCar(ImmutableEntity):
161
    ...     wheels = IntField(default=4, validation=lambda x: 3 <= x <= 4)
162
    ...     color = EnumField(Color)
163
    >>> icar = ImmutableCar.from_objects({'wheels': 3, 'color': 'blue'})
164
    >>> icar
165
    ImmutableCar(wheels=3, color=0)
166
167
    >>> icar.wheels = 4
168
    Traceback (most recent call last):
169
    AttributeError: Assignment not allowed. ImmutableCar is immutable.
170
171
    >>> class FixedWheelCar(Entity):
172
    ...     wheels = IntField(default=4, immutable=True)
173
    ...     color = EnumField(Color)
174
    >>> fwcar = FixedWheelCar.from_objects(icar)
175
    >>> fwcar.json()
176
    '{"wheels": 3, "color": 0}'
177
178
    >>> # repainting the car is easy
179
    >>> fwcar.color = Color.red
180
    >>> fwcar.color.name
181
    'red'
182
183
    >>> # can't really change the number of wheels though
184
    >>> fwcar.wheels = 18
185
    Traceback (most recent call last):
186
    AttributeError: The wheels field is immutable.
187
188
189
Chapter X: The del and null Weeds
190
---------------------------------
191
192
    >>> old_date = lambda: isoparse('1982-02-17')
193
    >>> class CarBattery(Entity):
194
    ...     # NOTE: default value can be a callable!
195
    ...     first_charge = DateField(required=False)  # default=None, nullable=False
196
    ...     latest_charge = DateField(default=old_date, nullable=True)  # required=True
197
    ...     expiration = DateField(default=old_date, required=False, nullable=False)
198
199
    >>> # starting point
200
    >>> battery = CarBattery()
201
    >>> battery
202
    CarBattery()
203
    >>> battery.json()
204
    '{"latest_charge": "1982-02-17T00:00:00", "expiration": "1982-02-17T00:00:00"}'
205
206
    >>> # first_charge is not assigned a default value. Once one is assigned, it can be deleted,
207
    >>> #   but it can't be made null.
208
    >>> battery.first_charge = isoparse('2016-03-23')
209
    >>> battery
210
    CarBattery(first_charge=datetime.datetime(2016, 3, 23, 0, 0))
211
    >>> battery.first_charge = None
212
    Traceback (most recent call last):
213
    ValidationError: Value for first_charge not given or invalid.
214
    >>> del battery.first_charge
215
    >>> battery
216
    CarBattery()
217
218
    >>> # latest_charge can be null, but it can't be deleted. The default value is a callable.
219
    >>> del battery.latest_charge
220
    Traceback (most recent call last):
221
    AttributeError: The latest_charge field is required and cannot be deleted.
222
    >>> battery.latest_charge = None
223
    >>> battery.json()
224
    '{"latest_charge": null, "expiration": "1982-02-17T00:00:00"}'
225
226
    >>> # expiration is assigned by default, can't be made null, but can be deleted.
227
    >>> battery.expiration
228
    datetime.datetime(1982, 2, 17, 0, 0)
229
    >>> battery.expiration = None
230
    Traceback (most recent call last):
231
    ValidationError: Value for expiration not given or invalid.
232
    >>> del battery.expiration
233
    >>> battery.json()
234
    '{"latest_charge": null}'
235
236
237
"""
238
from __future__ import absolute_import, division, print_function
239
240
from collections import Iterable
241
from datetime import datetime
242
from functools import reduce
243
from json import loads as json_loads, dumps as json_dumps
244
from logging import getLogger
245
246
from enum import Enum
247
from ._vendor.boltons.timeutils import isoparse
248
from .collection import AttrDict
249
from .compat import (with_metaclass, string_types, text_type, integer_types, iteritems,
250
                     itervalues, odict)
251
from .exceptions import ValidationError, Raise
252
from .ish import find_or_none
253
from .logz import DumpEncoder
254
from .type_coercion import maybecall
255
256
log = getLogger(__name__)
257
258
__all__ = [
259
    "Entity", "ImmutableEntity", "Field",
260
    "BooleanField", "BoolField", "IntegerField", "IntField",
261
    "NumberField", "StringField", "DateField",
262
    "EnumField", "ListField", "MapField", "ComposableField",
263
]
264
265
KEY_OVERRIDES_MAP = "__key_overrides__"
266
267
268
NOTES = """
269
270
Current deficiencies to schematics:
271
  - no get_mock_object method
272
  - no context-dependent serialization or MultilingualStringType
273
  - name = StringType(serialized_name='person_name', alternate_names=['human_name'])
274
  - name = StringType(serialize_when_none=False)
275
  - more flexible validation error messages
276
  - field validation can depend on other fields
277
  - 'roles' containing blacklists for .dump() and .json()
278
    __roles__ = {
279
        EntityRole.registered_name: Blacklist('field1', 'field2'),
280
        EntityRole.another_registered_name: Whitelist('field3', 'field4'),
281
    }
282
283
284
TODO:
285
  - alternate field names
286
  - add dump_if_null field option
287
  - add help/description parameter to Field
288
  - consider leveraging slots
289
  - collect all validation errors before raising
290
  - Allow returning string error message for validation instead of False
291
  - profile and optimize
292
  - use boltons instead of dateutil
293
294
295
Optional Field Properties:
296
  - validation = None
297
  - default = None
298
  - required = True
299
  - in_dump = True
300
  - nullable = False
301
302
Behaviors:
303
  - Nullable is a "hard" setting, in that the value is either always or never allowed to be None.
304
  - What happens then if required=False and nullable=False?
305
      - The object can be init'd without a value (though not with a None value).
306
        getattr throws AttributeError
307
      - Any assignment must be not None.
308
309
310
  - Setting a value to None doesn't "unset" a value.  (That's what del is for.)  And you can't
311
    del a value if required=True, nullable=False, default=None.
312
313
  - If a field is not required, del does *not* "unmask" the default value.  Instead, del
314
    removes the value from the object entirely.  To get back the default value, need to recreate
315
    the object.  Entity.from_objects(old_object)
316
317
318
  - Disabling in_dump is a "hard" setting, in that with it disabled the field will never get
319
    dumped.  With it enabled, the field may or may not be dumped depending on its value and other
320
    settings.
321
322
  - Required is a "hard" setting, in that if True, a valid value or default must be provided. None
323
    is only a valid value or default if nullable is True.
324
325
  - In general, nullable means that None is a valid value.
326
    - getattr returns None instead of raising Attribute error
327
    - If in_dump, field is given with null value.
328
    - If default is not None, assigning None clears a previous assignment. Future getattrs return
329
      the default value.
330
    - What does nullable mean with default=None and required=True? Does instantiation raise
331
      an error if assignment not made on init? Can IntField(nullable=True) be init'd?
332
333
  - If required=False and nullable=False, field will only be in dump if field!=None.
334
    Also, getattr raises AttributeError.
335
  - If required=False and nullable=True, field will be in dump if field==None.
336
337
  - If in_dump is True, does default value get dumped:
338
    - if no assignment, default exists
339
    - if nullable, and assigned None
340
  - How does optional validation work with nullable and assigning None?
341
  - When does gettattr throw AttributeError, and when does it return None?
342
343
344
345
"""
346
347
348
class Field(object):
349
    """
350
    Fields are doing something very similar to boxing and unboxing
351
    of c#/java primitives.  __set__ should take a "primitive" or "raw" value and create a "boxed"
352
    or "programatically useable" value of it.  While __get__ should return the boxed value,
353
    dump in turn should unbox the value into a primitive or raw value.
354
355
    Arguments:
356
        types_ (primitive literal or type or sequence of types):
357
        default (any, callable, optional):  If default is callable, it's guaranteed to return a
358
            valid value at the time of Entity creation.
359
        required (boolean, optional):
360
        validation (callable, optional):
361
        dump (boolean, optional):
362
    """
363
364
    # Used to track order of field declarations. Supporting python 2.7, so can't rely
365
    #   on __prepare__.  Strategy lifted from http://stackoverflow.com/a/4460034/2127762
366
    _order_helper = 0
367
368
    def __init__(self, default=None, required=True, validation=None,
369
                 in_dump=True, nullable=False, immutable=False):
370
        self._default = default if callable(default) else self.box(None, default)
371
        self._required = required
372
        self._validation = validation
373
        self._in_dump = in_dump
374
        self._nullable = nullable
375
        self._immutable = immutable
376
        if default is not None:
377
            self.validate(None, self.box(None, maybecall(default)))
378
379
        self._order_helper = Field._order_helper
380
        Field._order_helper += 1
381
382
    @property
383
    def name(self):
384
        try:
385
            return self._name
386
        except AttributeError:
387
            log.error("The name attribute has not been set for this field. "
388
                      "Call set_name at class creation time.")
389
            raise
390
391
    def set_name(self, name):
392
        self._name = name
393
        return self
394
395
    def __get__(self, instance, instance_type):
396
        try:
397
            if instance is None:  # if calling from the class object
398
                val = getattr(instance_type, KEY_OVERRIDES_MAP)[self.name]
399
            else:
400
                val = instance.__dict__[self.name]
401
        except AttributeError:
402
            log.error("The name attribute has not been set for this field.")
403
            raise AttributeError("The name attribute has not been set for this field.")
404
        except KeyError:
405
            if self.default is not None:
406
                val = maybecall(self.default)  # default *can* be a callable
407
            elif self._nullable:
408
                return None
409
            else:
410
                raise AttributeError("A value for {0} has not been set".format(self.name))
411
        if val is None and not self.nullable:
412
            # means the "tricky edge case" was activated in __delete__
413
            raise AttributeError("The {0} field has been deleted.".format(self.name))
414
        return self.unbox(instance, instance_type, val)
415
416
    def __set__(self, instance, val):
417
        if self.immutable and instance._initd:
418
            raise AttributeError("The {0} field is immutable.".format(self.name))
419
        # validate will raise an exception if invalid
420
        # validate will return False if the value should be removed
421
        instance.__dict__[self.name] = self.validate(instance, self.box(instance, val))
422
423
    def __delete__(self, instance):
424
        if self.immutable and instance._initd:
425
            raise AttributeError("The {0} field is immutable.".format(self.name))
426
        elif self.required:
427
            raise AttributeError("The {0} field is required and cannot be deleted."
428
                                 .format(self.name))
429
        elif not self.nullable:
430
            # tricky edge case
431
            # given a field Field(default='some value', required=False, nullable=False)
432
            # works together with Entity.dump() logic for selecting fields to include in dump
433
            # `if value is not None or field.nullable`
434
            instance.__dict__[self.name] = None
435
        else:
436
            instance.__dict__.pop(self.name, None)
437
438
    def box(self, instance, val):
439
        return val
440
441
    def unbox(self, instance, instance_type, val):
442
        return val
443
444
    def dump(self, val):
445
        return val
446
447
    def validate(self, instance, val):
448
        """
449
450
        Returns:
451
            True: if val is valid
452
453
        Raises:
454
            ValidationError
455
        """
456
        # note here calling, but not assigning; could lead to unexpected behavior
457
        if isinstance(val, self._type) and (self._validation is None or self._validation(val)):
458
            return val
459
        elif val is None and self.nullable:
460
            return val
461
        else:
462
            raise ValidationError(getattr(self, 'name', 'undefined name'), val)
463
464
    @property
465
    def required(self):
466
        return self._required
467
468
    @property
469
    def type(self):
470
        return self._type
471
472
    @property
473
    def default(self):
474
        return self._default
475
476
    @property
477
    def in_dump(self):
478
        return self._in_dump
479
480
    @property
481
    def nullable(self):
482
        return self.is_nullable
483
484
    @property
485
    def is_nullable(self):
486
        return self._nullable
487
488
    @property
489
    def immutable(self):
490
        return self._immutable
491
492
493
class BooleanField(Field):
494
    _type = bool
495
496
    def box(self, instance, val):
497
        return None if val is None else bool(val)
498
499
BoolField = BooleanField
500
501
502
class IntegerField(Field):
503
    _type = integer_types
504
505
IntField = IntegerField
506
507
508
class NumberField(Field):
509
    _type = integer_types + (float, complex)
510
511
512
class StringField(Field):
513
    _type = string_types
514
515
    def box(self, instance, val):
516
        return text_type(val) if isinstance(val, NumberField._type) else val
517
518
519
class DateField(Field):
520
    _type = datetime
521
522
    def box(self, instance, val):
523
        try:
524
            return isoparse(val) if isinstance(val, string_types) else val
525
        except ValueError as e:
526
            raise ValidationError(val, msg=e)
527
528
    def dump(self, val):
529
        return None if val is None else val.isoformat()
530
531
532
class EnumField(Field):
533
534
    def __init__(self, enum_class, default=None, required=True, validation=None,
535
                 in_dump=True, nullable=False, immutable=False):
536
        if not issubclass(enum_class, Enum):
537
            raise ValidationError(None, msg="enum_class must be an instance of Enum")
538
        self._type = enum_class
539
        super(EnumField, self).__init__(default, required, validation,
540
                                        in_dump, nullable, immutable)
541
542
    def box(self, instance, val):
543
        if val is None:
544
            # let the required/nullable logic handle validation for this case
545
            return None
546
        try:
547
            # try to box using val as an Enum name
548
            return val if isinstance(val, self._type) else self._type(val)
549
        except ValueError as e1:
550
            try:
551
                # try to box using val as an Enum value
552
                return self._type[val]
553
            except KeyError:
554
                raise ValidationError(val, msg=e1)
555
556
    def dump(self, val):
557
        return None if val is None else val.value
558
559
560
class ListField(Field):
561
    _type = tuple
562
563
    def __init__(self, element_type, default=None, required=True, validation=None,
564
                 in_dump=True, nullable=False, immutable=False):
565
        self._element_type = element_type
566
        super(ListField, self).__init__(default, required, validation,
567
                                        in_dump, nullable, immutable)
568
569
    def box(self, instance, val):
570
        if val is None:
571
            return None
572
        elif isinstance(val, string_types):
573
            raise ValidationError("Attempted to assign a string to ListField {0}"
574
                                  "".format(self.name))
575
        elif isinstance(val, Iterable):
576
            et = self._element_type
577
            if isinstance(et, type) and issubclass(et, Entity):
578
                return self._type(v if isinstance(v, et) else et(**v) for v in val)
579
            else:
580
                return self._type(val)
581
        else:
582
            raise ValidationError(val, msg="Cannot assign a non-iterable value to "
583
                                           "{0}".format(self.name))
584
585
    def unbox(self, instance, instance_type, val):
586
        return self._type() if val is None and not self.nullable else val
587
588
    def dump(self, val):
589
        if isinstance(self._element_type, type) and issubclass(self._element_type, Entity):
590
            return self._type(v.dump() for v in val)
591
        else:
592
            return val
593
594
    def validate(self, instance, val):
595
        if val is None:
596
            if not self.nullable:
597
                raise ValidationError(self.name, val)
598
            return None
599
        else:
600
            val = super(ListField, self).validate(instance, val)
601
            et = self._element_type
602
            self._type(Raise(ValidationError(self.name, el, et)) for el in val
603
                       if not isinstance(el, et))
604
            return val
605
606
607
class MutableListField(ListField):
608
    _type = list
609
610
611
class MapField(Field):
612
    _type = dict
613
    __eq__ = dict.__eq__
614
    __hash__ = dict.__hash__
615
616
617
class ComposableField(Field):
618
619
    def __init__(self, field_class, default=None, required=True, validation=None,
620
                 in_dump=True, nullable=False, immutable=False):
621
        self._type = field_class
622
        super(ComposableField, self).__init__(default, required, validation,
623
                                              in_dump, nullable, immutable)
624
625
    def box(self, instance, val):
626
        if val is None:
627
            return None
628
        if isinstance(val, self._type):
629
            return val
630
        else:
631
            # assuming val is a dict now
632
            try:
633
                # if there is a key named 'self', have to rename it
634
                val['slf'] = val.pop('self')
635
            except KeyError:
636
                pass  # no key of 'self', so no worries
637
            return val if isinstance(val, self._type) else self._type(**val)
638
639
    def dump(self, val):
640
        return None if val is None else val.dump()
641
642
643
class EntityType(type):
644
645
    @staticmethod
646
    def __get_entity_subclasses(bases):
647
        try:
648
            return [base for base in bases if issubclass(base, Entity) and base is not Entity]
649
        except NameError:
650
            # NameError: global name 'Entity' is not defined
651
            return ()
652
653
    def __new__(mcs, name, bases, dct):
654
        # if we're about to mask a field that's already been created with something that's
655
        #  not a field, then assign it to an alternate variable name
656
        non_field_keys = (key for key, value in iteritems(dct)
657
                          if not isinstance(value, Field) and not key.startswith('__'))
658
        entity_subclasses = EntityType.__get_entity_subclasses(bases)
659
        if entity_subclasses:
660
            keys_to_override = [key for key in non_field_keys
661
                                if any(isinstance(base.__dict__.get(key), Field)
662
                                       for base in entity_subclasses)]
663
            dct[KEY_OVERRIDES_MAP] = dict((key, dct.pop(key)) for key in keys_to_override)
664
        else:
665
            dct[KEY_OVERRIDES_MAP] = dict()
666
667
        return super(EntityType, mcs).__new__(mcs, name, bases, dct)
668
669
    def __init__(cls, name, bases, attr):
670
        super(EntityType, cls).__init__(name, bases, attr)
671
        cls.__fields__ = odict(cls.__fields__) if hasattr(cls, '__fields__') else odict()
672
        cls.__fields__.update(sorted(((name, field.set_name(name))
673
                                      for name, field in iteritems(cls.__dict__)
674
                                      if isinstance(field, Field)),
675
                                     key=lambda item: item[1]._order_helper))
676
        if hasattr(cls, '__register__'):
677
            cls.__register__()
678
679
    def __call__(cls, *args, **kwargs):
680
        instance = super(EntityType, cls).__call__(*args, **kwargs)
681
        setattr(instance, '_{0}__initd'.format(cls.__name__), True)
682
        return instance
683
684
    @property
685
    def fields(cls):
686
        return cls.__fields__.keys()
687
688
689
@with_metaclass(EntityType)
690
class Entity(object):
691
    __fields__ = odict()
692
693
    def __init__(self, **kwargs):
694
        for key, field in iteritems(self.__fields__):
695
            try:
696
                setattr(self, key, kwargs[key])
697
            except KeyError:
698
                # handle the case of fields inherited from subclass but overrode on class object
699
                if key in getattr(self, KEY_OVERRIDES_MAP):
700
                    setattr(self, key, getattr(self, KEY_OVERRIDES_MAP)[key])
701
                elif field.required and field.default is None:
702
                    raise ValidationError(key, msg="{0} requires a {1} field. Instantiated with "
703
                                                   "{2}".format(self.__class__.__name__,
704
                                                                key, kwargs))
705
            except ValidationError:
706
                if kwargs[key] is not None or field.required:
707
                    raise
708
        self.validate()
709
710
    @classmethod
711
    def from_objects(cls, *objects, **override_fields):
712
        init_vars = dict()
713
        search_maps = tuple(AttrDict(o) if isinstance(o, dict) else o
714
                            for o in ((override_fields,) + objects))
715
        for key in cls.__fields__:
716
            init_vars[key] = find_or_none(key, search_maps)
717
        return cls(**init_vars)
718
719
    @classmethod
720
    def from_json(cls, json_str):
721
        return cls(**json_loads(json_str))
722
723
    @classmethod
724
    def load(cls, data_dict):
725
        return cls(**data_dict)
726
727
    def validate(self):
728
        # TODO: here, validate should only have to determine if the required keys are set
729
        try:
730
            reduce(lambda _, name: getattr(self, name),
731
                   (name for name, field in iteritems(self.__fields__) if field.required)
732
                   )
733
        except TypeError as e:
734
            if str(e) == "reduce() of empty sequence with no initial value":
735
                pass
736
        except AttributeError as e:
737
            raise ValidationError(None, msg=e)
738
739
    def __repr__(self):
740
        def _valid(key):
741
            if key.startswith('_'):
742
                return False
743
            try:
744
                getattr(self, key)
745
                return True
746
            except AttributeError:
747
                return False
748
749
        def _val(key):
750
            val = getattr(self, key)
751
            return repr(val.value) if isinstance(val, Enum) else repr(val)
752
753
        def _sort_helper(key):
754
            field = self.__fields__.get(key)
755
            return field._order_helper if field is not None else -1
756
757
        kwarg_str = ", ".join("{0}={1}".format(key, _val(key))
758
                              for key in sorted(self.__dict__, key=_sort_helper)
759
                              if _valid(key))
760
        return "{0}({1})".format(self.__class__.__name__, kwarg_str)
761
762
    @classmethod
763
    def __register__(cls):
764
        pass
765
766
    def json(self, indent=None, separators=None, **kwargs):
767
        return json_dumps(self, indent=indent, separators=separators, cls=DumpEncoder, **kwargs)
768
769
    def pretty_json(self, indent=2, separators=(',', ': '), **kwargs):
770
        return self.json(indent=indent, separators=separators, **kwargs)
771
772
    def dump(self):
773
        return odict((field.name, field.dump(value))
774
                     for field, value in ((field, getattr(self, field.name, None))
775
                                          for field in self.__dump_fields())
776
                     if value is not None or field.nullable)
777
778
    @classmethod
779
    def __dump_fields(cls):
780
        if '__dump_fields_cache' not in cls.__dict__:
781
            cls.__dump_fields_cache = tuple(field for field in itervalues(cls.__fields__)
782
                                            if field.in_dump)
783
        return cls.__dump_fields_cache
784
785
    def __eq__(self, other):
786
        if self.__class__ != other.__class__:
787
            return False
788
        rando_default = 19274656290  # need an arbitrary but definite value if field does not exist
789
        return all(getattr(self, field, rando_default) == getattr(other, field, rando_default)
790
                   for field in self.__fields__)
791
792
    def __hash__(self):
793
        return sum(hash(getattr(self, field, None)) for field in self.__fields__)
794
795
    @property
796
    def _initd(self):
797
        return getattr(self, '_{0}__initd'.format(self.__class__.__name__), None)
798
799
800
class ImmutableEntity(Entity):
801
802
    def __setattr__(self, attribute, value):
803
        if self._initd:
804
            raise AttributeError("Assignment not allowed. {0} is immutable."
805
                                 .format(self.__class__.__name__))
806
        super(ImmutableEntity, self).__setattr__(attribute, value)
807
808
    def __delattr__(self, item):
809
        if self._initd:
810
            raise AttributeError("Deletion not allowed. {0} is immutable."
811
                                 .format(self.__class__.__name__))
812
        super(ImmutableEntity, self).__delattr__(item)
813