Completed
Push — develop ( 23a79f...0cce3e )
by Kale
57s
created

auxlib.Field.validate()   B

Complexity

Conditions 6

Size

Total Lines 16

Duplication

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