-
Notifications
You must be signed in to change notification settings - Fork 11
/
Copy pathmodels.py
309 lines (248 loc) · 8.41 KB
/
models.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
import sys
import pkg_resources
from copy import deepcopy
from urllib2 import urlparse
import uuid
from confmodel.config import Config, ConfigField
from confmodel.errors import ConfigError
from confmodel.fallbacks import SingleFieldFallback
version_info = {
'language': 'python',
'language_version_string': sys.version,
'language_version': '%d.%d.%d' % (
sys.version_info.major,
sys.version_info.minor,
sys.version_info.micro,
),
'package': 'elastic-git',
'package_version': pkg_resources.require('elastic-git')[0].version
}
class ModelField(ConfigField):
default_mapping = {
'type': 'string',
}
def __init__(self, doc, required=False, default=None, static=False,
fallbacks=(), mapping={}, name=None):
super(ModelField, self).__init__(
doc, required=required, default=default, static=static,
fallbacks=fallbacks)
self.name = name
self.mapping = self.__class__.default_mapping.copy()
self.mapping.update(mapping)
def __repr__(self):
return '<%s.%s %r>' % (
self.__class__.__module__, self.__class__.__name__, self.name)
class TextField(ModelField):
"""
A text field
"""
field_type = 'str'
def clean(self, value):
if not isinstance(value, basestring):
self.raise_config_error("is not a base string.")
return value
class UnicodeTextField(ModelField):
"""
A text field
"""
field_type = 'unicode'
def clean(self, value):
if not isinstance(value, unicode):
self.raise_config_error("is not unicode.")
return value
class IntegerField(ModelField):
"""
An integer field
"""
field_type = 'int'
#: Mapping for Elasticsearch
default_mapping = {
'type': 'integer',
}
def clean(self, value):
try:
# We go via "str" to avoid silently truncating floats.
# XXX: Is there a better way to do this?
return int(str(value))
except (ValueError, TypeError):
self.raise_config_error("could not be converted to int.")
class FloatField(ModelField):
"""
A float field
"""
field_type = 'float'
#: Mapping for Elasticsearch
default_mapping = {
'type': 'float'
}
def clean(self, value):
try:
return float(value)
except (ValueError, TypeError):
self.raise_config_error("could not be converted to float.")
class BooleanField(ModelField):
"""
A boolean field
"""
field_type = 'bool'
#: Mapping for Elasticsearch
default_mapping = {
'type': 'boolean'
}
def clean(self, value):
if isinstance(value, basestring):
return value.strip().lower() not in ('false', '0', '')
return bool(value)
class ListField(ModelField):
"""
A list field
"""
field_type = 'list'
#: Mapping for Elasticsearch
default_mapping = {
'type': 'string',
}
def __init__(self, doc, fields, default=[], static=False,
fallbacks=(), mapping={}):
super(ListField, self).__init__(
doc, default=default, static=static, fallbacks=fallbacks,
mapping=mapping)
self.fields = fields
def clean(self, value):
if isinstance(value, tuple):
value = list(value)
if not isinstance(value, list):
self.raise_config_error("is not a list.")
if len(value) > 0:
for field in self.fields:
if not any([field.clean(v) for v in value]):
self.raise_config_error(
'All field checks failed for some values.')
return deepcopy(value)
class DictField(ModelField):
"""
A dictionary field
"""
field_type = 'dict'
def __init__(self, doc, fields, default=None, static=False,
fallbacks=(), mapping=()):
mapping = mapping or self.generate_default_mapping(fields)
super(DictField, self).__init__(
doc, default=default, static=static, fallbacks=fallbacks,
mapping=mapping)
self.fields = fields
def generate_default_mapping(self, fields):
field_names = [field.name for field in fields]
return {
'type': 'nested',
'properties': dict(
[(name, {'type': 'string'}) for name in field_names]),
}
def clean(self, value):
if not isinstance(value, dict):
self.raise_config_error('is not a dict.')
return deepcopy(value)
def validate(self, config):
data = self.get_value(config)
if data:
for key, value in data.items():
[field] = [field for field in self.fields if field.name == key]
field.clean(value)
class URLField(ModelField):
"""
A url field
"""
field_type = 'URL'
#: Mapping for Elasticsearch
mapping = {
'type': 'string',
}
def clean(self, value):
if not isinstance(value, basestring):
self.raise_config_error("is not a URL string.")
# URLs must be bytes, not unicode.
if isinstance(value, unicode):
value = value.encode('utf-8')
return urlparse.urlparse(value)
class UUIDField(TextField):
def validate(self, config):
config._config_data.setdefault(self.name, uuid.uuid4().hex)
return super(UUIDField, self).validate(config)
class Model(Config):
"""
Base model for all things stored in Git and Elasticsearch.
A very thin wrapper around :py:class:`confmodel.Config`.
Subclass this model and add more field as needed.
:param dict config_data:
A dictionary with keys & values to populate this Model
instance with.
"""
_version = DictField(
'Model Version Identifier',
default=version_info,
fields=(
TextField('language', name='language'),
TextField('language_version_string',
name='language_version_string'),
TextField('language_version', name='language_version'),
TextField('package', name='package'),
TextField('package_version', name='package_version'),
),
mapping={
'type': 'nested',
'properties': {
'language': {'type': 'string'},
'language_version_string': {'type': 'string'},
'language_version': {'type': 'string'},
'package': {'type': 'string'},
'package_version': {'type': 'string'}
}
})
uuid = UUIDField('Unique Identifier')
def __init__(self, config_data, static=False, es_meta=None):
super(Model, self).__init__(config_data, static=static)
self._read_only = False
self.es_meta = es_meta
def __eq__(self, other):
own_data = dict(self)
other_data = dict(other)
own_version_info = own_data.pop('_version')
other_version_info = other_data.pop('_version')
return (own_data == other_data and
own_version_info == other_version_info)
def update(self, fields, mark_read_only=True):
model_class = self.__class__
data = dict(self)
data.update(fields)
new_instance = model_class(data)
if mark_read_only:
self.set_read_only()
return new_instance
def set_read_only(self):
"""
Mark this model instance as being read only.
Returns self to allow it to be chainable.
:returns: self
"""
self._read_only = True
return self
def is_read_only(self):
return self._read_only
def __iter__(self):
for field in self._get_fields():
yield field.name, field.get_value(self)
def compatible_version(self, own_version, check_version):
own = map(int, own_version.split('.'))
check = map(int, check_version.split('.'))
return own >= check
def post_validate(self):
value = self._version
current_version = version_info['package_version']
package_version = value['package_version']
if not self.compatible_version(current_version, package_version):
raise ConfigError(
'Got a version from the future, expecting: %r got %r' % (
current_version, package_version))
super(Model, self).post_validate()
ConfigError
SingleFieldFallback