1 # -*- coding: utf-8 -*-
3 # Copyright (C) 2013 LEAP
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 generic configuration handlers
30 #from leap.base.util.translations import LEAPTranslatable
31 from leap.common.check import leap_assert
34 logger = logging.getLogger(__name__)
37 __all__ = ['PluggableConfig',
40 'UnknownOptionException',
41 'MissingValueException',
42 'ConfigurationProviderException',
48 class UnknownOptionException(Exception):
49 """exception raised when a non-configuration
50 value is present in the configuration"""
53 class MissingValueException(Exception):
54 """exception raised when a required value is missing"""
57 class ConfigurationProviderException(Exception):
58 """exception raised when a configuration provider is missing, etc"""
61 class TypeCastException(Exception):
62 """exception raised when a
63 configuration item cannot be coerced to a type"""
66 class ConfigAdaptor(object):
68 abstract base class for config adaotors for
69 serialization/deserialization and custom validation
72 def read(self, filename):
73 raise NotImplementedError("abstract base class")
75 def write(self, config, filename):
76 with open(filename, 'w') as f:
77 self._write(f, config)
79 def _write(self, fp, config):
80 raise NotImplementedError("abstract base class")
82 def validate(self, config, schema):
83 raise NotImplementedError("abstract base class")
89 class JSONSchemaEncoder(json.JSONEncoder):
91 custom default encoder that
92 casts python objects to json objects for
95 def default(self, obj):
110 class JSONAdaptor(ConfigAdaptor):
112 extensions = ['json']
114 def read(self, _from):
115 if isinstance(_from, file):
116 _from_string = _from.read()
117 if isinstance(_from, str):
119 return json.loads(_from_string)
121 def _write(self, fp, config):
122 fp.write(json.dumps(config,
126 def validate(self, config, schema_obj):
127 schema_json = JSONSchemaEncoder().encode(schema_obj)
128 schema = json.loads(schema_json)
129 jsonschema.validate(config, schema)
132 adaptors['json'] = JSONAdaptor()
137 # Allow to apply a predefined set of types to the
138 # specs, so it checks the validity of formats and cast it
139 # to proper python types.
145 class DateType(object):
148 def to_python(self, data):
149 return time.strptime(data, self.fmt)
151 def get_prep_value(self, data):
152 return time.strftime(self.fmt, data)
155 class TranslatableType(object):
157 a type that casts to LEAPTranslatable objects.
158 Used for labels we get from providers and stuff.
161 def to_python(self, data):
162 # TODO: add translatable
163 return data # LEAPTranslatable(data)
165 # needed? we already have an extended dict...
166 #def get_prep_value(self, data):
170 class URIType(object):
172 def to_python(self, data):
173 parsed = urlparse.urlparse(data)
174 if not parsed.scheme:
175 raise TypeCastException("uri %s has no schema" % data)
176 return parsed.geturl()
178 def get_prep_value(self, data):
182 class HTTPSURIType(object):
184 def to_python(self, data):
185 parsed = urlparse.urlparse(data)
186 if not parsed.scheme:
187 raise TypeCastException("uri %s has no schema" % data)
188 if parsed.scheme != "https":
189 raise TypeCastException(
190 "uri %s does not has "
191 "https schema" % data)
192 return parsed.geturl()
194 def get_prep_value(self, data):
201 'https-uri': HTTPSURIType(),
202 'translatable': TranslatableType(),
206 class PluggableConfig(object):
216 self.adaptors = adaptors
218 self._format = format
223 def option_dict(self):
224 if hasattr(self, 'options') and isinstance(self.options, dict):
225 return self.options.get('properties', None)
231 if isinstance(self.option_dict, dict):
232 return self.option_dict.items()
235 def validate(self, config, format=None):
239 schema = self.options
241 format = self._format
244 adaptor = self.get_adaptor(self._format)
245 adaptor.validate(config, schema)
247 # we really should make format mandatory...
248 logger.error('no format passed to validate')
250 # first round of validation is ok.
251 # now we proceed to cast types if any specified.
252 self.to_python(config)
254 def to_python(self, config):
256 cast types following first type and then format indications.
258 unseen_options = [i for i in config if i not in self.option_dict]
260 raise UnknownOptionException(
261 "Unknown options: %s" % ', '.join(unseen_options))
263 for key, value in config.items():
264 _type = self.option_dict[key].get('type')
265 if _type is None and 'default' in self.option_dict[key]:
266 _type = type(self.option_dict[key]['default'])
267 if _type is not None:
269 if not callable(_type) and isinstance(value, _type):
273 config[key] = _type(value)
274 except BaseException, e:
275 raise TypeCastException(
276 "Could not coerce %s, %s, "
277 "to type %s: %s" % (key, value, _type.__name__, e))
278 _format = self.option_dict[key].get('format', None)
279 _ftype = self.types.get(_format, None)
282 config[key] = _ftype.to_python(value)
283 except BaseException, e:
284 raise TypeCastException(
285 "Could not coerce %s, %s, "
286 "to format %s: %s" % (key, value,
287 _ftype.__class__.__name__,
292 def prep_value(self, config):
294 the inverse of to_python method,
295 called just before serialization
297 for key, value in config.items():
298 _format = self.option_dict[key].get('format', None)
299 _ftype = self.types.get(_format, None)
300 if _ftype and hasattr(_ftype, 'get_prep_value'):
302 config[key] = _ftype.get_prep_value(value)
303 except BaseException, e:
304 raise TypeCastException(
305 "Could not serialize %s, %s, "
306 "by format %s: %s" % (key, value,
307 _ftype.__class__.__name__,
313 # methods for adding configuration
315 def get_default_values(self):
317 return a config options from configuration defaults
320 for key, value in self.items():
321 if 'default' in value:
322 defaults[key] = value['default']
323 return copy.deepcopy(defaults)
325 def get_adaptor(self, format):
327 get specified format adaptor or
328 guess for a given filename
330 adaptor = self.adaptors.get(format, None)
334 # not registered in adaptors dict, let's try all
335 for adaptor in self.adaptors.values():
336 if format in adaptor.extensions:
339 def filename2format(self, filename):
340 extension = os.path.splitext(filename)[-1]
341 return extension.lstrip('.') or None
343 def serialize(self, filename, format=None, full=False):
345 format = self._format
347 format = self.filename2format(filename)
349 raise Exception('Please specify a format')
350 # TODO: more specific exception type
352 adaptor = self.get_adaptor(format)
354 raise Exception("Adaptor not found for format: %s" % format)
356 config = copy.deepcopy(self.config)
357 serializable = self.prep_value(config)
358 adaptor.write(serializable, filename)
361 self.touch_mtime(filename)
363 def touch_mtime(self, filename):
365 os.utime(filename, (mtime, mtime))
367 def deserialize(self, string=None, fromfile=None, format=None):
369 load configuration from a file or string
372 def _try_deserialize():
374 with open(fromfile, 'r') as f:
375 content = adaptor.read(f)
377 content = adaptor.read(string)
383 leap_assert(os.path.exists(fromfile))
385 format = self.filename2format(fromfile)
388 format = self._format
390 adaptor = self.get_adaptor(format)
395 content = _try_deserialize()
398 # no adaptor, let's try rest of adaptors
400 adaptors = self.adaptors[:]
405 format in x.extensions),
408 for adaptor in adaptors:
409 content = _try_deserialize()
418 def load(self, *args, **kwargs):
420 load from string or file
421 if no string of fromfile option is given,
422 it will attempt to load from defaults
423 defined in the schema.
425 string = args[0] if args else None
426 fromfile = kwargs.get("fromfile", None)
427 mtime = kwargs.pop("mtime", None)
431 # start with defaults, so we can
432 # have partial values applied.
433 content = self.get_default_values()
434 if string and isinstance(string, str):
435 content = self.deserialize(string)
437 if not string and fromfile is not None:
438 #import ipdb;ipdb.set_trace()
439 content = self.deserialize(fromfile=fromfile)
442 logger.error('no content could be loaded')
446 # lazy evaluation until first level of nesting
447 # to allow lambdas with context-dependant info
448 # like os.path.expanduser
449 for k, v in content.iteritems():
453 self.validate(content)
454 self.config = content
458 def testmain(): # pragma: no cover
460 from tests import test_validation as t
463 config = PluggableConfig(_format="json")
464 properties = copy.deepcopy(t.sample_spec)
466 config.options = properties
467 config.load(fromfile='data.json')
470 pprint.pprint(config.config)
472 config.serialize('/tmp/testserial.json')
474 if __name__ == "__main__":