Skip to content

Commit 053196f

Browse files
committed
Create a model structure for requests
1 parent 0a10fa9 commit 053196f

3 files changed

Lines changed: 292 additions & 0 deletions

File tree

consulate/models/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# coding=utf-8
2+
"""Consulate Data Models"""

consulate/models/base.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
# coding=utf-8
2+
"""
3+
Base Model
4+
5+
"""
6+
import collections
7+
8+
9+
class Model(collections.Iterable):
10+
"""A model contains an __attribute__ map that defines the name,
11+
its type for type validation, an optional validation method, a method
12+
used to
13+
14+
.. python::
15+
16+
class MyModel(Model):
17+
18+
__attributes__ = {
19+
'ID': {
20+
'type': uuid.UUID,
21+
'required': False,
22+
'default': None,
23+
'cast_from': str,
24+
'cast_to': str
25+
},
26+
'Serial': {
27+
'type': int
28+
'required': True,
29+
'default': 0,
30+
'validator': lambda v: v >= 0 end,
31+
}
32+
}
33+
34+
"""
35+
36+
__attributes__ = {}
37+
"""The attributes that define the data elements of the model"""
38+
39+
def __init__(self, **kwargs):
40+
super(Model, self).__init__()
41+
[setattr(self, key, value) for key, value in kwargs.items()]
42+
[self._set_default(k) for k in self.__attributes__.keys()
43+
if k not in kwargs.keys()]
44+
45+
def __iter__(self):
46+
"""Iterate through the model's key, value pairs.
47+
48+
:rtype: iterator
49+
50+
"""
51+
for name in self.__attributes__.keys():
52+
value = self._maybe_cast_value(name)
53+
if value is not None:
54+
yield self._maybe_return_key(name), value
55+
56+
def __setattr__(self, name, value):
57+
"""Set the value for an attribute of the model, validating the
58+
attribute name and its type if the type is defined in ``__types__``.
59+
60+
:param str name: The attribute name
61+
:param mixed value: The value to set
62+
:raises: AttributeError
63+
:raises: TypeError
64+
:raises: ValueError
65+
66+
"""
67+
if name not in self.__attributes__:
68+
raise AttributeError('Invalid attribute "{}"'.format(name))
69+
value = self._validate_value(name, value)
70+
super(Model, self).__setattr__(name, value)
71+
72+
def _maybe_cast_value(self, name):
73+
"""Return the attribute value, possibly cast to a different type if
74+
the ``cast_to`` item is set in the attribute definition.
75+
76+
:param str name: The attribute name
77+
:rtype: mixed
78+
79+
"""
80+
value = getattr(self, name)
81+
if value is not None and self.__attributes__[name].get('cast_to'):
82+
return self.__attributes__[name]['cast_to'](value)
83+
return value
84+
85+
def _maybe_return_key(self, name):
86+
"""Return the attribute name as specified in it's ``key`` definition,
87+
if specified. This is to map python attribute names to their Consul
88+
alternatives.
89+
90+
:param str name: The attribute name
91+
:rtype: mixed
92+
93+
"""
94+
if self.__attributes__[name].get('key'):
95+
return self.__attributes__[name]['key']
96+
return name
97+
98+
def _required_attr(self, name):
99+
"""Returns :data:`True` if the attribute is required.
100+
101+
:param str name: The attribute name
102+
:rtype: bool
103+
104+
"""
105+
return self.__attributes__[name].get('required', False)
106+
107+
def _set_default(self, name):
108+
"""Set the default value for the attribute name.
109+
110+
:param str name: The attribute name
111+
112+
"""
113+
setattr(self, name, self.__attributes__[name].get('default', None))
114+
115+
def _validate_value(self, name, value):
116+
"""Ensures the the value validates based upon the type or a validation
117+
function, raising an error if it does not.
118+
119+
:param str name: The attribute name
120+
:param mixed value: The value that is being set
121+
:rtype: mixed
122+
:raises: TypeError
123+
:raises: ValueError
124+
125+
"""
126+
if value is None and self._required_attr(name):
127+
raise ValueError('Attribute "{}" is required'.format(name))
128+
129+
if value is not None:
130+
if not isinstance(value, self.__attributes__[name].get('type')):
131+
cast_from = self.__attributes__[name].get('cast_from')
132+
if cast_from and isinstance(value, cast_from):
133+
value = self.__attributes__[name]['type'](value)
134+
else:
135+
raise TypeError(
136+
'Attribute "{}" must be of type {} not {}'.format(
137+
name, self.__attributes__[name]['type'].__name__,
138+
value.__class__.__name__))
139+
140+
if self.__attributes__[name].get('enum') \
141+
and value not in self.__attributes__[name]['enum']:
142+
raise ValueError(
143+
'Attribute "{}" value {!r} not valid'.format(name, value))
144+
145+
validator = self.__attributes__[name].get('validator')
146+
if callable(validator):
147+
if not validator(value):
148+
raise ValueError(
149+
'Attribute "{}" value {!r} did not validate'.format(
150+
name, value))
151+
return value

tests/base_model_tests.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# coding=utf-8
2+
"""Tests for the Base Model"""
3+
import unittest
4+
import uuid
5+
6+
from consulate.models import base
7+
8+
9+
class TestModel(base.Model):
10+
"""Model to perform tests against"""
11+
__slots__ = ['id', 'serial', 'name', 'value']
12+
__attributes__ = {
13+
'id': {
14+
'key': 'ID',
15+
'type': uuid.UUID,
16+
'cast_from': str,
17+
'cast_to': str,
18+
},
19+
'serial': {
20+
'key': 'Serial',
21+
'type': int,
22+
'default': 0,
23+
'required': True,
24+
'validator': lambda v: v >= 0,
25+
},
26+
'name': {
27+
'key': 'Name',
28+
'type': str,
29+
'required': True
30+
},
31+
'value': {
32+
'type': str
33+
},
34+
'type': {
35+
'key': 'Type',
36+
'type': str,
37+
'enum': {'client', 'server'}
38+
}
39+
}
40+
41+
42+
class TestCase(unittest.TestCase):
43+
44+
def test_happy_case_with_defaults(self):
45+
kwargs = {
46+
'id': uuid.uuid4(),
47+
'name': str(uuid.uuid4())
48+
}
49+
model = TestModel(**kwargs)
50+
for key, value in kwargs.items():
51+
self.assertEqual(getattr(model, key), value)
52+
self.assertEqual(model.serial, 0)
53+
54+
def test_happy_case_with_all_values(self):
55+
kwargs = {
56+
'id': uuid.uuid4(),
57+
'serial': 1,
58+
'name': str(uuid.uuid4()),
59+
'value': str(uuid.uuid4())
60+
}
61+
model = TestModel(**kwargs)
62+
for key, value in kwargs.items():
63+
self.assertEqual(getattr(model, key), value)
64+
65+
def test_cast_from_str(self):
66+
expectation = uuid.uuid4()
67+
kwargs = {
68+
'id': str(expectation),
69+
'name': str(uuid.uuid4())
70+
}
71+
model = TestModel(**kwargs)
72+
self.assertEqual(model.id, expectation)
73+
74+
def test_validator_failure(self):
75+
kwargs = {
76+
'id': uuid.uuid4(),
77+
'name': str(uuid.uuid4()),
78+
'serial': -1
79+
}
80+
with self.assertRaises(ValueError):
81+
TestModel(**kwargs)
82+
83+
def test_type_failure(self):
84+
kwargs = {
85+
'id': True,
86+
'name': str(uuid.uuid4())
87+
}
88+
with self.assertRaises(TypeError):
89+
TestModel(**kwargs)
90+
91+
def test_missing_requirement(self):
92+
with self.assertRaises(ValueError):
93+
TestModel()
94+
95+
def test_invalid_attribute(self):
96+
kwargs = {'name': str(uuid.uuid4()), 'foo': 'bar'}
97+
with self.assertRaises(AttributeError):
98+
TestModel(**kwargs)
99+
100+
def test_invalid_attribute_assignment(self):
101+
kwargs = {'name': str(uuid.uuid4())}
102+
model = TestModel(**kwargs)
103+
with self.assertRaises(AttributeError):
104+
model.foo = 'bar'
105+
106+
def test_invalid_enum_assignment(self):
107+
kwargs = {'name': str(uuid.uuid4()), 'type': 'invalid'}
108+
with self.assertRaises(ValueError):
109+
TestModel(**kwargs)
110+
111+
def test_cast_to_dict(self):
112+
kwargs = {
113+
'id': uuid.uuid4(),
114+
'serial': 1,
115+
'name': str(uuid.uuid4()),
116+
'value': str(uuid.uuid4()),
117+
'type': 'client'
118+
}
119+
expectation = {
120+
'ID': str(kwargs['id']),
121+
'Serial': kwargs['serial'],
122+
'Name': kwargs['name'],
123+
'value': kwargs['value'],
124+
'Type': kwargs['type']
125+
}
126+
model = TestModel(**kwargs)
127+
self.assertDictEqual(dict(model), expectation)
128+
129+
def test_cast_to_dict_only_requirements(self):
130+
kwargs = {
131+
'serial': 1,
132+
'name': str(uuid.uuid4())
133+
}
134+
expectation = {
135+
'Serial': kwargs['serial'],
136+
'Name': kwargs['name']
137+
}
138+
model = TestModel(**kwargs)
139+
self.assertDictEqual(dict(model), expectation)

0 commit comments

Comments
 (0)