You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@ariatosca.apache.org by ra...@apache.org on 2016/11/15 16:05:11 UTC
[10/13] incubator-ariatosca git commit: ARIA-18 Migrate DSL parser
and TOSCA extension code
http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/8ee1470e/aria/parser/presentation/fields.py
----------------------------------------------------------------------
diff --git a/aria/parser/presentation/fields.py b/aria/parser/presentation/fields.py
new file mode 100644
index 0000000..4669fee
--- /dev/null
+++ b/aria/parser/presentation/fields.py
@@ -0,0 +1,754 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import threading
+from functools import wraps
+from types import MethodType
+from collections import OrderedDict
+
+from ..exceptions import InvalidValueError, AriaException
+from ..utils import (FrozenList, FrozenDict, print_exception, deepcopy_with_locators, merge,
+ cachedmethod, puts, as_raw, full_type_name, safe_repr)
+from .null import NULL
+from .utils import validate_primitive
+
+#
+# Class decorators
+#
+
+# pylint: disable=unused-argument
+
+def has_fields(cls):
+ """
+ Class decorator for validated field support.
+
+ 1. Adds a :code:`FIELDS` class property that is a dict of all the fields.
+ Will inherit and merge :code:`FIELDS` properties from base classes if
+ they have them.
+
+ 2. Generates automatic :code:`@property` implementations for the fields
+ with the help of a set of special function decorators.
+
+ The class also works with the Python dict protocol, so that
+ fields can be accessed via dict semantics. The functionality is
+ identical to that of using attribute access.
+
+ The class will also gain two utility methods, :code:`_iter_field_names`
+ and :code:`_iter_fields`.
+ """
+
+ # Make sure we have FIELDS
+ if 'FIELDS' not in cls.__dict__:
+ setattr(cls, 'FIELDS', OrderedDict())
+
+ # Inherit FIELDS from base classes
+ for base in cls.__bases__:
+ if hasattr(base, 'FIELDS'):
+ cls.FIELDS.update(base.FIELDS)
+
+ # We could do this:
+ #
+ # for name, field in cls.__dict__.iteritems():
+ #
+ # But dir() is better because it has a deterministic order (alphabetical)
+
+ for name in dir(cls):
+ field = getattr(cls, name)
+
+ if isinstance(field, Field):
+ # Accumulate
+ cls.FIELDS[name] = field
+
+ field.name = name
+ field.container_cls = cls
+
+ # This function is here just to create an enclosed scope for "field"
+ def closure(field):
+
+ # By convention, we have the getter wrap the original function.
+ # (It is, for example, where the Python help() function will look for
+ # docstrings when encountering a property.)
+ @cachedmethod
+ @wraps(field.func)
+ def getter(self):
+ return field.get(self, None)
+
+ def setter(self, value):
+ field.set(self, None, value)
+
+ # Convert to Python property
+ return property(fget=getter, fset=setter)
+
+ setattr(cls, name, closure(field))
+
+ # Bind methods
+ setattr(cls, '_iter_field_names', MethodType(has_fields_iter_field_names, None, cls))
+ setattr(cls, '_iter_fields', MethodType(has_fields_iter_fields, None, cls))
+
+ # Behave like a dict
+ setattr(cls, '__len__', MethodType(has_fields_len, None, cls))
+ setattr(cls, '__getitem__', MethodType(has_fields_getitem, None, cls))
+ setattr(cls, '__setitem__', MethodType(has_fields_setitem, None, cls))
+ setattr(cls, '__delitem__', MethodType(has_fields_delitem, None, cls))
+ setattr(cls, '__iter__', MethodType(has_fields_iter, None, cls))
+ setattr(cls, '__contains__', MethodType(has_fields_contains, None, cls))
+
+ return cls
+
+
+def short_form_field(name):
+ """
+ Class decorator for specifying the short form field.
+
+ The class must be decorated with :func:`has_fields`.
+ """
+
+ def decorator(cls):
+ if hasattr(cls, name) and hasattr(cls, 'FIELDS') and (name in cls.FIELDS):
+ setattr(cls, 'SHORT_FORM_FIELD', name)
+ return cls
+ else:
+ raise AttributeError('@short_form_field must be used with '
+ 'a Field name in @has_fields class')
+ return decorator
+
+
+def allow_unknown_fields(cls):
+ """
+ Class decorator specifying that the class allows unknown fields.
+
+ The class must be decorated with :func:`has_fields`.
+ """
+
+ if hasattr(cls, 'FIELDS'):
+ setattr(cls, 'ALLOW_UNKNOWN_FIELDS', True)
+ return cls
+ else:
+ raise AttributeError('@allow_unknown_fields must be used with a @has_fields class')
+
+#
+# Method decorators
+#
+
+
+def primitive_field(cls=None, default=None, allowed=None, required=False):
+ """
+ Method decorator for primitive fields.
+
+ The function must be a method in a class decorated with :func:`has_fields`.
+ """
+
+ def decorator(func):
+ return Field(field_variant='primitive', func=func, cls=cls, default=default,
+ allowed=allowed, required=required)
+ return decorator
+
+
+def primitive_list_field(cls=None, default=None, allowed=None, required=False):
+ """
+ Method decorator for list of primitive fields.
+
+ The function must be a method in a class decorated with :func:`has_fields`.
+ """
+
+ def decorator(func):
+ return Field(field_variant='primitive_list', func=func, cls=cls, default=default,
+ allowed=allowed, required=required)
+ return decorator
+
+
+def primitive_dict_field(cls=None, default=None, allowed=None, required=False):
+ """
+ Method decorator for dict of primitive fields.
+
+ The function must be a method in a class decorated with :func:`has_fields`.
+ """
+ def decorator(func):
+ return Field(field_variant='primitive_dict', func=func, cls=cls, default=default,
+ allowed=allowed, required=required)
+ return decorator
+
+
+def primitive_dict_unknown_fields(cls=None, default=None, allowed=None, required=False):
+ """
+ Method decorator for dict of primitive fields, for all the fields that are
+ not already decorated.
+
+ The function must be a method in a class decorated with :func:`has_fields`.
+ """
+
+ def decorator(func):
+ return Field(field_variant='primitive_dict_unknown_fields', func=func, cls=cls,
+ default=default, allowed=allowed, required=required)
+ return decorator
+
+
+def object_field(cls, default=None, allowed=None, required=False):
+ """
+ Method decorator for object fields.
+
+ The function must be a method in a class decorated with :func:`has_fields`.
+ """
+ def decorator(func):
+ return Field(field_variant='object', func=func, cls=cls, default=default, allowed=allowed,
+ required=required)
+ return decorator
+
+
+def object_list_field(cls, default=None, allowed=None, required=False):
+ """
+ Method decorator for list of object fields.
+
+ The function must be a method in a class decorated with :func:`has_fields`.
+ """
+
+ def decorator(func):
+ return Field(field_variant='object_list', func=func, cls=cls, default=default,
+ allowed=allowed, required=required)
+ return decorator
+
+
+def object_dict_field(cls, default=None, allowed=None, required=False):
+ """
+ Method decorator for dict of object fields.
+
+ The function must be a method in a class decorated with :func:`has_fields`.
+ """
+
+ def decorator(func):
+ return Field(field_variant='object_dict', func=func, cls=cls, default=default,
+ allowed=allowed, required=required)
+ return decorator
+
+
+def object_sequenced_list_field(cls, default=None, allowed=None, required=False):
+ """
+ Method decorator for sequenced list of object fields.
+
+ The function must be a method in a class decorated with :func:`has_fields`.
+ """
+
+ def decorator(func):
+ return Field(field_variant='sequenced_object_list', func=func, cls=cls, default=default,
+ allowed=allowed, required=required)
+ return decorator
+
+
+def object_dict_unknown_fields(cls, default=None, allowed=None, required=False):
+ """
+ Method decorator for dict of object fields, for all the fields that are not already decorated.
+
+ The function must be a method in a class decorated with :func:`has_fields`.
+ """
+ def decorator(func):
+ return Field(field_variant='object_dict_unknown_fields', func=func, cls=cls,
+ default=default, allowed=allowed, required=required)
+ return decorator
+
+
+def field_getter(getter_func):
+ """
+ Method decorator for overriding the getter function of a field.
+
+ The signature of the getter function must be: :code:`f(field, presentation, context)`.
+ The default getter can be accessed as :code:`field.default_get(presentation, context)`.
+
+ The function must already be decorated with a field decorator.
+ """
+
+ def decorator(field):
+ if isinstance(field, Field):
+ field.get = MethodType(getter_func, field, Field)
+ return field
+ else:
+ raise AttributeError('@field_getter must be used with a Field')
+ return decorator
+
+
+def field_setter(setter_func):
+ """
+ Method decorator for overriding the setter function of a field.
+
+ The signature of the setter function must be: :code:`f(field, presentation, context, value)`.
+ The default setter can be accessed as :code:`field.default_set(presentation, context, value)`.
+
+ The function must already be decorated with a field decorator.
+ """
+
+ def decorator(field):
+ if isinstance(field, Field):
+ field.set = MethodType(setter_func, field, Field)
+ return field
+ else:
+ raise AttributeError('@field_setter must be used with a Field')
+ return decorator
+
+
+def field_validator(validator_fn):
+ """
+ Method decorator for overriding the validator function of a field.
+
+ The signature of the validator function must be: :code:f(field, presentation, context)`.
+ The default validator can be accessed as :code:`field.default_validate(presentation, context)`.
+
+ The function must already be decorated with a field decorator.
+ """
+
+ def decorator(field):
+ if isinstance(field, Field):
+ field.validate = MethodType(validator_fn, field, Field)
+ return field
+ else:
+ raise AttributeError('@field_validator must be used with a Field')
+ return decorator
+
+#
+# Utils
+#
+
+
+def has_fields_iter_field_names(self):
+ for name in self.__class__.FIELDS:
+ yield name
+
+
+def has_fields_iter_fields(self):
+ return self.FIELDS.iteritems()
+
+
+def has_fields_len(self):
+ return len(self.__class__.FIELDS)
+
+
+def has_fields_getitem(self, key):
+ if not isinstance(key, basestring):
+ raise TypeError('key must be a string')
+ if key not in self.__class__.FIELDS:
+ raise KeyError('no \'%s\' property' % key)
+ return getattr(self, key)
+
+
+def has_fields_setitem(self, key, value):
+ if not isinstance(key, basestring):
+ raise TypeError('key must be a string')
+ if key not in self.__class__.FIELDS:
+ raise KeyError('no \'%s\' property' % key)
+ return setattr(self, key, value)
+
+
+def has_fields_delitem(self, key):
+ if not isinstance(key, basestring):
+ raise TypeError('key must be a string')
+ if key not in self.__class__.FIELDS:
+ raise KeyError('no \'%s\' property' % key)
+ return setattr(self, key, None)
+
+
+def has_fields_iter(self):
+ return self.__class__.FIELDS.iterkeys()
+
+
+def has_fields_contains(self, key):
+ if not isinstance(key, basestring):
+ raise TypeError('key must be a string')
+ return key in self.__class__.FIELDS
+
+
+class Field(object):
+ """
+ Field handler used by :code:`@has_fields` decorator.
+ """
+
+ def __init__(self, field_variant, func, cls=None, default=None, allowed=None, required=False):
+ if cls == str:
+ # Use "unicode" instead of "str"
+ cls = unicode
+
+ self.container_cls = None
+ self.name = None
+ self.field_variant = field_variant
+ self.func = func
+ self.cls = cls
+ self.default = default
+ self.allowed = allowed
+ self.required = required
+
+ @property
+ def full_name(self):
+ return 'field "%s" in "%s"' % (self.name, full_type_name(self.container_cls))
+
+ @property
+ def full_cls_name(self):
+ name = full_type_name(self.cls)
+ if name == 'unicode':
+ # For simplicity, display "unicode" as "str"
+ name = 'str'
+ return name
+
+ def get(self, presentation, context):
+ return self.default_get(presentation, context)
+
+ def set(self, presentation, context, value):
+ return self.default_set(presentation, context, value)
+
+ def validate(self, presentation, context):
+ self.default_validate(presentation, context)
+
+ def get_locator(self, raw):
+ if hasattr(raw, '_locator'):
+ locator = raw._locator
+ if locator is not None:
+ return locator.get_child(self.name)
+ return None
+
+ def dump(self, presentation, context):
+ value = getattr(presentation, self.name)
+ if value is None:
+ return
+
+ dumper = getattr(self, '_dump_%s' % self.field_variant)
+ dumper(context, value)
+
+ def default_get(self, presentation, context):
+ # Handle raw
+
+ default_raw = (presentation._get_default_raw()
+ if hasattr(presentation, '_get_default_raw')
+ else None)
+
+ if default_raw is None:
+ raw = presentation._raw
+ else:
+ # Handle default raw value
+ raw = deepcopy_with_locators(default_raw)
+ merge(raw, presentation._raw)
+
+ # Handle unknown fields
+
+ if self.field_variant == 'primitive_dict_unknown_fields':
+ return self._get_primitive_dict_unknown_fields(presentation, raw, context)
+ elif self.field_variant == 'object_dict_unknown_fields':
+ return self._get_object_dict_unknown_fields(presentation, raw, context)
+
+ is_short_form_field = (self.container_cls.SHORT_FORM_FIELD == self.name
+ if hasattr(self.container_cls, 'SHORT_FORM_FIELD')
+ else False)
+ is_dict = isinstance(raw, dict)
+
+ # Find value
+
+ value = self._find_value(is_short_form_field, is_dict, raw)
+
+ # Handle required
+
+ if value is None:
+ if self.required:
+ raise InvalidValueError('required %s does not have a value' % self.full_name,
+ locator=self.get_locator(raw))
+ else:
+ return None
+
+ # Handle allowed values
+
+ if self.allowed is not None:
+ if value not in self.allowed:
+ raise InvalidValueError('%s is not %s'
+ % (self.full_name, ' or '.join([safe_repr(v)
+ for v in self.allowed])),
+ locator=self.get_locator(raw))
+
+ # Handle get according to variant
+
+ getter = getattr(self, '_get_%s' % self.field_variant, None)
+
+ if getter is None:
+ locator = self.get_locator(raw)
+ location = (' @%s' % locator) if locator is not None else ''
+ raise AttributeError('%s has unsupported field variant: "%s"%s'
+ % (self.full_name, self.field_variant, location))
+
+ return getter(presentation, raw, value, context)
+
+ def _find_value(self, is_short_form_field, is_dict, raw):
+ value = None
+ if is_short_form_field and not is_dict:
+ # Handle short form
+ value = raw
+ elif is_dict:
+ if self.name in raw:
+ value = raw[self.name]
+ if value is None:
+ # An explicit null
+ value = NULL
+ else:
+ value = self.default
+ return value
+
+ def default_set(self, presentation, context, value):
+ raw = presentation._raw
+ old = self.get(presentation, context)
+ raw[self.name] = value
+ try:
+ self.validate(presentation, context)
+ except Exception as e:
+ raw[self.name] = old
+ raise e
+ return old
+
+ def default_validate(self, presentation, context):
+ value = None
+
+ try:
+ value = self.get(presentation, context)
+ except AriaException as e:
+ if e.issue:
+ context.validation.report(issue=e.issue)
+ except Exception as e:
+ context.validation.report(exception=e)
+ print_exception(e)
+
+ self.validate_value(value, context)
+
+ def validate_value(self, value, context):
+ if isinstance(value, list):
+ if self.field_variant == 'object_list':
+ for element in value:
+ if hasattr(element, '_validate'):
+ element._validate(context)
+ elif self.field_variant == 'sequenced_object_list':
+ for _, element in value:
+ if hasattr(element, '_validate'):
+ element._validate(context)
+ elif isinstance(value, dict):
+ if self.field_variant in ('object_dict', 'object_dict_unknown_fields'):
+ for inner_value in value.itervalues():
+ if hasattr(inner_value, '_validate'):
+ inner_value._validate(context)
+
+ if hasattr(value, '_validate'):
+ value._validate(context)
+
+ @staticmethod
+ def _get_context():
+ thread_locals = threading.local()
+ return getattr(thread_locals, 'aria_consumption_context', None)
+
+ def _coerce_primitive(self, value, context):
+ if context is None:
+ context = Field._get_context()
+ allow_primitive_coercion = (context.validation.allow_primitive_coersion
+ if context is not None
+ else True)
+ return validate_primitive(value, self.cls, allow_primitive_coercion)
+
+ # primitive
+
+ def _get_primitive(self, presentation, raw, value, context):
+ if (self.cls is not None and not isinstance(value, self.cls)
+ and value is not None and value is not NULL):
+ try:
+ return self._coerce_primitive(value, context)
+ except ValueError as e:
+ raise InvalidValueError('%s is not a valid "%s": %s' %
+ (self.full_name, self.full_cls_name, safe_repr(value)),
+ locator=self.get_locator(raw), cause=e)
+ return value
+
+ def _dump_primitive(self, context, value):
+ if hasattr(value, 'as_raw'):
+ value = as_raw(value)
+ puts('%s: %s' % (self.name, context.style.literal(value)))
+
+ # primitive list
+
+ def _get_primitive_list(self, presentation, raw, value, context):
+ if not isinstance(value, list):
+ raise InvalidValueError('%s is not a list: %s' % (self.full_name, safe_repr(value)),
+ locator=self.get_locator(raw))
+ primitive_list = value
+ if self.cls is not None:
+ if context is None:
+ context = Field._get_context()
+ primitive_list = []
+ for i, _ in enumerate(value):
+ primitive = value[i]
+ try:
+ primitive = self._coerce_primitive(primitive, context)
+ except ValueError as e:
+ raise InvalidValueError('%s is not a list of "%s": element %d is %s'
+ % (self.full_name,
+ self.full_cls_name,
+ i,
+ safe_repr(primitive)),
+ locator=self.get_locator(raw), cause=e)
+ if primitive in primitive_list:
+ raise InvalidValueError('%s has a duplicate "%s": %s'
+ % (self.full_name,
+ self.full_cls_name,
+ safe_repr(primitive)),
+ locator=self.get_locator(raw))
+ primitive_list.append(primitive)
+ return FrozenList(primitive_list)
+
+ def _dump_primitive_list(self, context, value):
+ puts('%s:' % self.name)
+ with context.style.indent:
+ for primitive in value:
+ if hasattr(primitive, 'as_raw'):
+ primitive = as_raw(primitive)
+ puts(context.style.literal(primitive))
+
+ # primitive dict
+
+ def _get_primitive_dict(self, presentation, raw, value, context):
+ if not isinstance(value, dict):
+ raise InvalidValueError('%s is not a dict: %s' % (self.full_name, safe_repr(value)),
+ locator=self.get_locator(raw))
+ primitive_dict = value
+ if self.cls is not None:
+ if context is None:
+ context = Field._get_context()
+ primitive_dict = OrderedDict()
+ for k, v in value.iteritems():
+ try:
+ primitive_dict[k] = self._coerce_primitive(v, context)
+ except ValueError as e:
+ raise InvalidValueError('%s is not a dict of "%s" values: entry "%d" is %s'
+ % (self.full_name, self.full_cls_name, k, safe_repr(v)),
+ locator=self.get_locator(raw),
+ cause=e)
+ return FrozenDict(primitive_dict)
+
+ def _dump_primitive_dict(self, context, value):
+ puts('%s:' % self.name)
+ with context.style.indent:
+ for v in value.itervalues():
+ if hasattr(v, 'as_raw'):
+ v = as_raw(v)
+ puts(context.style.literal(v))
+
+ # object
+
+ def _get_object(self, presentation, raw, value, context):
+ try:
+ return self.cls(name=self.name, raw=value, container=presentation)
+ except TypeError as e:
+ raise InvalidValueError('%s cannot not be initialized to an instance of "%s": %s'
+ % (self.full_name, self.full_cls_name, safe_repr(value)),
+ cause=e,
+ locator=self.get_locator(raw))
+
+ def _dump_object(self, context, value):
+ puts('%s:' % self.name)
+ with context.style.indent:
+ if hasattr(value, '_dump'):
+ value._dump(context)
+
+ # object list
+
+ def _get_object_list(self, presentation, raw, value, context):
+ if not isinstance(value, list):
+ raise InvalidValueError('%s is not a list: %s'
+ % (self.full_name, safe_repr(value)),
+ locator=self.get_locator(raw))
+ return FrozenList((self.cls(name=self.name, raw=v, container=presentation) for v in value))
+
+ def _dump_object_list(self, context, value):
+ puts('%s:' % self.name)
+ with context.style.indent:
+ for v in value:
+ if hasattr(v, '_dump'):
+ v._dump(context)
+
+ # object dict
+
+ def _get_object_dict(self, presentation, raw, value, context):
+ if not isinstance(value, dict):
+ raise InvalidValueError('%s is not a dict: %s' % (self.full_name, safe_repr(value)),
+ locator=self.get_locator(raw))
+ return FrozenDict(((k, self.cls(name=k, raw=v, container=presentation))
+ for k, v in value.iteritems()))
+
+ def _dump_object_dict(self, context, value):
+ puts('%s:' % self.name)
+ with context.style.indent:
+ for v in value.itervalues():
+ if hasattr(v, '_dump'):
+ v._dump(context)
+
+ # sequenced object list
+
+ def _get_sequenced_object_list(self, presentation, raw, value, context):
+ if not isinstance(value, list):
+ raise InvalidValueError('%s is not a sequenced list (a list of dicts, '
+ 'each with exactly one key): %s'
+ % (self.full_name, safe_repr(value)),
+ locator=self.get_locator(raw))
+ sequence = []
+ for v in value:
+ if not isinstance(v, dict):
+ raise InvalidValueError('%s list elements are not all dicts with '
+ 'exactly one key: %s' % (self.full_name, safe_repr(value)),
+ locator=self.get_locator(raw))
+ if len(v) != 1:
+ raise InvalidValueError('%s list elements do not all have exactly one key: %s'
+ % (self.full_name, safe_repr(value)),
+ locator=self.get_locator(raw))
+ key, value = v.items()[0]
+ sequence.append((key, self.cls(name=key, raw=value, container=presentation)))
+ return FrozenList(sequence)
+
+ def _dump_sequenced_object_list(self, context, value):
+ puts('%s:' % self.name)
+ for _, v in value:
+ if hasattr(v, '_dump'):
+ v._dump(context)
+
+ # primitive dict for unknown fields
+
+ def _get_primitive_dict_unknown_fields(self, presentation, raw, context):
+ if isinstance(raw, dict):
+ primitive_dict = raw
+ if self.cls is not None:
+ if context is None:
+ context = Field._get_context()
+ primitive_dict = OrderedDict()
+ for k, v in raw.iteritems():
+ if k not in presentation.FIELDS:
+ try:
+ primitive_dict[k] = self._coerce_primitive(v, context)
+ except ValueError as e:
+ raise InvalidValueError('%s is not a dict of "%s" values:'
+ ' entry "%d" is %s'
+ % (self.full_name, self.full_cls_name,
+ k, safe_repr(v)),
+ locator=self.get_locator(raw),
+ cause=e)
+ return FrozenDict(primitive_dict)
+ return None
+
+ def _dump_primitive_dict_unknown_fields(self, context, value):
+ self._dump_primitive_dict(context, value)
+
+ # object dict for unknown fields
+
+ def _get_object_dict_unknown_fields(self, presentation, raw, context):
+ if isinstance(raw, dict):
+ return FrozenDict(((k, self.cls(name=k, raw=v, container=presentation))
+ for k, v in raw.iteritems() if k not in presentation.FIELDS))
+ return None
+
+ def _dump_object_dict_unknown_fields(self, context, value):
+ self._dump_object_dict(context, value)
http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/8ee1470e/aria/parser/presentation/null.py
----------------------------------------------------------------------
diff --git a/aria/parser/presentation/null.py b/aria/parser/presentation/null.py
new file mode 100644
index 0000000..a69134e
--- /dev/null
+++ b/aria/parser/presentation/null.py
@@ -0,0 +1,67 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from ..utils import deepcopy_with_locators
+
+
+class Null(object):
+ """
+ Represents an explicit null value provided by the user, which is different from
+ not supplying a value at all.
+
+ It is a singleton.
+ """
+
+ @property
+ def as_raw(self):
+ return None
+
+NULL = Null()
+
+
+def none_to_null(value):
+ """
+ Convert :code:`None` to :code:`NULL`, recursively.
+ """
+
+ if value is None:
+ return NULL
+ if isinstance(value, list):
+ value = deepcopy_with_locators(value)
+ for i, _ in enumerate(value):
+ value[i] = none_to_null(value[i])
+ elif isinstance(value, dict):
+ value = deepcopy_with_locators(value)
+ for k, v in value.iteritems():
+ value[k] = none_to_null(v)
+ return value
+
+
+def null_to_none(value):
+ """
+ Convert :code:`NULL` to :code:`None`, recursively.
+ """
+
+ if value is NULL:
+ return None
+ if isinstance(value, list):
+ value = deepcopy_with_locators(value)
+ for i, _ in enumerate(value):
+ value[i] = none_to_null(value[i])
+ elif isinstance(value, dict):
+ value = deepcopy_with_locators(value)
+ for k, v in value.iteritems():
+ value[k] = none_to_null(v)
+ return value
http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/8ee1470e/aria/parser/presentation/presentation.py
----------------------------------------------------------------------
diff --git a/aria/parser/presentation/presentation.py b/aria/parser/presentation/presentation.py
new file mode 100644
index 0000000..15cb5ed
--- /dev/null
+++ b/aria/parser/presentation/presentation.py
@@ -0,0 +1,235 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from ..validation import Issue
+from ..utils import HasCachedMethods, full_type_name, deepcopy_with_locators, puts, safe_repr
+from .null import none_to_null
+from .utils import (get_locator, validate_no_short_form, validate_no_unknown_fields,
+ validate_known_fields, validate_primitive)
+
+
+class Value(object):
+ """
+ Encapsulates a typed value assignment.
+ """
+
+ def __init__(self, type_name, value, description):
+ self.type = deepcopy_with_locators(type_name)
+ self.value = deepcopy_with_locators(value)
+ self.description = deepcopy_with_locators(description)
+
+
+class PresentationBase(HasCachedMethods):
+ """
+ Base class for ARIA presentation classes.
+ """
+
+ def __init__(self, name=None, raw=None, container=None):
+ self._name = name
+ self._raw = raw
+ self._container = container
+ super(PresentationBase, self).__init__()
+
+ @property
+ def as_raw(self):
+ return self._raw
+
+ def _validate(self, context):
+ """
+ Validates the presentation while reporting errors in the validation context but
+ *not* raising exceptions.
+
+ The base class does not thing, but subclasses may override this for specialized
+ validation.
+ """
+
+ @property
+ def _fullname(self):
+ """
+ Always returns a usable full name of the presentation, whether it itself is named,
+ or recursing to its container, and finally defaulting to the class name.
+ """
+
+ if self._name is not None:
+ return self._name
+ elif self._container is not None:
+ return self._container._fullname
+ return full_type_name(self)
+
+ @property
+ def _locator(self):
+ """
+ Attempts to return the most relevant locator, whether we have one, or recursing
+ to our container.
+
+ :rtype: :class:`aria.reading.Locator`
+ """
+
+ return get_locator(self._raw, self._container)
+
+ def _get(self, *names):
+ """
+ Gets attributes recursively.
+ """
+
+ obj = self
+ if (obj is not None) and names:
+ for name in names:
+ obj = getattr(obj, name, None)
+ if obj is None:
+ break
+ return obj
+
+ def _get_from_dict(self, *names):
+ """
+ Gets attributes recursively, except for the last name which is used
+ to get a value from the last dict.
+ """
+
+ if names:
+ obj = self._get(*names[:-1])
+ if isinstance(obj, dict):
+ return obj.get(names[-1]) # pylint: disable=no-member
+ return None
+
+ def _get_child_locator(self, *names):
+ """
+ Attempts to return the locator of one our children. Will default to our locator
+ if not found.
+
+ :rtype: :class:`aria.reading.Locator`
+ """
+
+ if hasattr(self._raw, '_locator'):
+ locator = self._raw._locator
+ if locator is not None:
+ return locator.get_child(*names)
+ return self._locator
+
+ def _dump(self, context):
+ """
+ Emits a colorized representation.
+
+ The base class will emit a sensible default representation of the fields,
+ (by calling :code:`_dump_content`), but subclasses may override this for specialized
+ dumping.
+ """
+
+ if self._name:
+ puts(context.style.node(self._name))
+ with context.style.indent:
+ self._dump_content(context)
+ else:
+ self._dump_content(context)
+
+ def _dump_content(self, context, field_names=None):
+ """
+ Emits a colorized representation of the contents.
+
+ The base class will call :code:`_dump_field` on all the fields, but subclasses may
+ override this for specialized dumping.
+ """
+
+ if field_names:
+ for field_name in field_names:
+ self._dump_field(context, field_name)
+ elif hasattr(self, '_iter_field_names'):
+ for field_name in self._iter_field_names(): # pylint: disable=no-member
+ self._dump_field(context, field_name)
+ else:
+ puts(context.style.literal(self._raw))
+
+ def _dump_field(self, context, field_name):
+ """
+ Emits a colorized representation of the field.
+
+ According to the field type, this may trigger nested recursion. The nested
+ types will delegate to their :code:`_dump` methods.
+ """
+
+ field = self.FIELDS[field_name] # pylint: disable=no-member
+ field.dump(self, context)
+
+ def _clone(self, container=None):
+ """
+ Creates a clone of this presentation, optionally allowing for a new container.
+ """
+
+ raw = deepcopy_with_locators(self._raw)
+ if container is None:
+ container = self._container
+ return self.__class__(name=self._name, raw=raw, container=container)
+
+
+class Presentation(PresentationBase):
+ """
+ Base class for ARIA presentations. A presentation is a Pythonic wrapper around
+ agnostic raw data, adding the ability to read and modify the data with proper
+ validation.
+
+ ARIA presentation classes will often be decorated with @has_fields, as that
+ mechanism automates a lot of field-specific validation. However, that is not a
+ requirement.
+
+ Make sure that your utility property and method names begin with a "_", because
+ those names without a "_" prefix are normally reserved for fields.
+ """
+
+ def _validate(self, context):
+ validate_no_short_form(context, self)
+ validate_no_unknown_fields(context, self)
+ validate_known_fields(context, self)
+
+
+class AsIsPresentation(PresentationBase):
+ """
+ Base class for trivial ARIA presentations that provide the raw value as is.
+ """
+
+ def __init__(self, name=None, raw=None, container=None, cls=None):
+ super(AsIsPresentation, self).__init__(name, raw, container)
+ self.cls = cls
+
+ @property
+ def value(self):
+ return none_to_null(self._raw)
+
+ @value.setter
+ def value(self, value):
+ self._raw = value
+
+ @property
+ def _full_cls_name(self):
+ name = full_type_name(self.cls) if self.cls is not None else None
+ if name == 'unicode':
+ # For simplicity, display "unicode" as "str"
+ name = 'str'
+ return name
+
+ def _validate(self, context):
+ try:
+ validate_primitive(self._raw, self.cls, context.validation.allow_primitive_coersion)
+ except ValueError as e:
+ context.validation.report('"%s" is not a valid "%s": %s'
+ % (self._fullname, self._full_cls_name, safe_repr(self._raw)),
+ locator=self._locator,
+ level=Issue.FIELD,
+ exception=e)
+
+ def _dump(self, context):
+ if hasattr(self._raw, '_dump'):
+ self._raw._dump(context)
+ else:
+ super(AsIsPresentation, self)._dump(context)
http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/8ee1470e/aria/parser/presentation/presenter.py
----------------------------------------------------------------------
diff --git a/aria/parser/presentation/presenter.py b/aria/parser/presentation/presenter.py
new file mode 100644
index 0000000..4cade3a
--- /dev/null
+++ b/aria/parser/presentation/presenter.py
@@ -0,0 +1,69 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from ..validation import Issue
+from ..utils import merge, safe_repr
+from .presentation import Presentation
+
+
+class Presenter(Presentation):
+ """
+ Base class for ARIA presenters.
+
+ Presenters provide a robust API over agnostic raw data.
+ """
+
+ DSL_VERSIONS = None
+ ALLOWED_IMPORTED_DSL_VERSIONS = None
+
+ @classmethod
+ def can_present(cls, raw):
+ dsl = raw.get('tosca_definitions_version')
+ assert cls.DSL_VERSIONS
+ return dsl in cls.DSL_VERSIONS
+
+ def _validate_import(self, context, presentation):
+ tosca_definitions_version = presentation.service_template.tosca_definitions_version
+ assert self.ALLOWED_IMPORTED_DSL_VERSIONS
+ if tosca_definitions_version is not None \
+ and tosca_definitions_version not in self.__class__.ALLOWED_IMPORTED_DSL_VERSIONS:
+ context.validation.report(
+ 'import "tosca_definitions_version" is not one of %s: %s'
+ % (' or '.join([safe_repr(v)
+ for v in self.__class__.ALLOWED_IMPORTED_DSL_VERSIONS]),
+ presentation.service_template.tosca_definitions_version),
+ locator=presentation._get_child_locator('inputs'),
+ level=Issue.BETWEEN_TYPES)
+ return False
+ return True
+
+ def _merge_import(self, presentation):
+ merge(self._raw, presentation._raw)
+ if hasattr(self._raw, '_locator') and hasattr(presentation._raw, '_locator'):
+ self._raw._locator.merge(presentation._raw._locator)
+
+ def _link_locators(self):
+ if hasattr(self._raw, '_locator'):
+ locator = self._raw._locator
+ delattr(self._raw, '_locator')
+ locator.link(self._raw)
+
+ @staticmethod
+ def _get_import_locations(context):
+ raise NotImplementedError
+
+ @staticmethod
+ def _get_deployment_template(context):
+ raise NotImplementedError
http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/8ee1470e/aria/parser/presentation/source.py
----------------------------------------------------------------------
diff --git a/aria/parser/presentation/source.py b/aria/parser/presentation/source.py
new file mode 100644
index 0000000..6f195d0
--- /dev/null
+++ b/aria/parser/presentation/source.py
@@ -0,0 +1,47 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from .exceptions import PresenterNotFoundError
+
+PRESENTER_CLASSES = []
+
+
+class PresenterSource(object):
+ """
+ Base class for ARIA presenter sources.
+
+ Presenter sources provide appropriate :class:`Presenter` classes for agnostic raw data.
+ """
+
+ def get_presenter(self, raw): # pylint: disable=unused-argument,no-self-use
+ raise PresenterNotFoundError('presenter not found')
+
+
+class DefaultPresenterSource(PresenterSource):
+ """
+ The default ARIA presenter source supports TOSCA Simple Profile.
+ """
+
+ def __init__(self, classes=None):
+ if classes is None:
+ classes = PRESENTER_CLASSES
+ self.classes = classes
+
+ def get_presenter(self, raw):
+ for cls in self.classes:
+ if cls.can_present(raw):
+ return cls
+
+ return super(DefaultPresenterSource, self).get_presenter(raw)
http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/8ee1470e/aria/parser/presentation/utils.py
----------------------------------------------------------------------
diff --git a/aria/parser/presentation/utils.py b/aria/parser/presentation/utils.py
new file mode 100644
index 0000000..2e4d873
--- /dev/null
+++ b/aria/parser/presentation/utils.py
@@ -0,0 +1,186 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from types import FunctionType
+
+from ..validation import Issue
+from ..utils import full_type_name, safe_repr
+from .null import NULL
+
+
+def get_locator(*values):
+ """
+ Gets the first available locator.
+
+ :rtype: :class:`aria.reading.Locator`
+ """
+
+ for v in values:
+ if hasattr(v, '_locator'):
+ locator = v._locator
+ if locator is not None:
+ return locator
+ return None
+
+
+def parse_types_dict_names(types_dict_names):
+ """
+ If the first element in the array is a function, extracts it out.
+ """
+
+ convert = None
+ if isinstance(types_dict_names[0], FunctionType):
+ convert = types_dict_names[0]
+ types_dict_names = types_dict_names[1:]
+ return types_dict_names, convert
+
+
+def validate_primitive(value, cls, coerce=False):
+ """
+ Checks if the value is of the primitive type, optionally attempting to coerce it
+ if it is not.
+
+ Raises a :code:`ValueError` if it isn't or if coercion failed.
+ """
+
+ if (cls is not None) and (value is not None) and (value is not NULL):
+ if (cls is unicode) or (cls is str): # These two types are interchangeable
+ valid = isinstance(value, basestring)
+ elif cls is int:
+ # In Python, a bool is an int
+ valid = isinstance(value, int) and not isinstance(value, bool)
+ else:
+ valid = isinstance(value, cls)
+ if not valid:
+ if coerce:
+ value = cls(value)
+ else:
+ raise ValueError('not a "%s": %s' % (full_type_name(cls), safe_repr(value)))
+ return value
+
+
+def validate_no_short_form(context, presentation):
+ """
+ Makes sure that we can use short form definitions only if we allowed it.
+ """
+
+ if not hasattr(presentation, 'SHORT_FORM_FIELD') and not isinstance(presentation._raw, dict):
+ context.validation.report('short form not allowed for field "%s"' % presentation._fullname,
+ locator=presentation._locator,
+ level=Issue.BETWEEN_FIELDS)
+
+
+def validate_no_unknown_fields(context, presentation):
+ """
+ Make sure that we can use unknown fields only if we allowed it.
+ """
+
+ if not getattr(presentation, 'ALLOW_UNKNOWN_FIELDS', False) \
+ and not context.validation.allow_unknown_fields \
+ and isinstance(presentation._raw, dict) \
+ and hasattr(presentation, 'FIELDS'):
+ for k in presentation._raw:
+ if k not in presentation.FIELDS:
+ context.validation.report('field "%s" is not supported in "%s"'
+ % (k, presentation._fullname),
+ locator=presentation._get_child_locator(k),
+ level=Issue.BETWEEN_FIELDS)
+
+
+def validate_known_fields(context, presentation):
+ """
+ Validates all known fields.
+ """
+
+ if hasattr(presentation, '_iter_fields'):
+ for _, field in presentation._iter_fields():
+ field.validate(presentation, context)
+
+
+def get_parent_presentation(context, presentation, *types_dict_names):
+ """
+ Returns the parent presentation according to the :code:`derived_from` field, or None if invalid.
+
+ Checks that we do not derive from ourselves and that we do not cause a circular hierarchy.
+
+ The arguments from the third onwards are used to locate a nested field under
+ :code:`service_template` under the root presenter. The first of these can optionally
+ be a function, in which case it will be called to convert type names. This can be used
+ to support shorthand type names, aliases, etc.
+ """
+
+ type_name = presentation.derived_from
+
+ if type_name is None:
+ return None
+
+ types_dict_names, convert = parse_types_dict_names(types_dict_names)
+ types_dict = context.presentation.get('service_template', *types_dict_names) or {}
+
+ if convert:
+ type_name = convert(context, type_name, types_dict)
+
+ # Make sure not derived from self
+ if type_name == presentation._name:
+ return None
+ # Make sure derived from type exists
+ elif type_name not in types_dict:
+ return None
+ else:
+ # Make sure derivation hierarchy is not circular
+ hierarchy = [presentation._name]
+ presentation_copy = presentation
+ while presentation_copy.derived_from is not None:
+ derived_from = presentation_copy.derived_from
+ if convert:
+ derived_from = convert(context, derived_from, types_dict)
+
+ if derived_from == presentation_copy._name or derived_from not in types_dict:
+ return None
+ presentation_copy = types_dict[derived_from]
+ if presentation_copy._name in hierarchy:
+ return None
+ hierarchy.append(presentation_copy._name)
+
+ return types_dict[type_name]
+
+
+def report_issue_for_unknown_type(context, presentation, type_name, field_name, value=None):
+ if value is None:
+ value = getattr(presentation, field_name)
+ context.validation.report('"%s" refers to an unknown %s in "%s": %s'
+ % (field_name, type_name, presentation._fullname, safe_repr(value)),
+ locator=presentation._get_child_locator(field_name),
+ level=Issue.BETWEEN_TYPES)
+
+
+def report_issue_for_parent_is_self(context, presentation, field_name):
+ context.validation.report('parent type of "%s" is self' % presentation._fullname,
+ locator=presentation._get_child_locator(field_name),
+ level=Issue.BETWEEN_TYPES)
+
+
+def report_issue_for_unknown_parent_type(context, presentation, field_name):
+ context.validation.report('unknown parent type "%s" in "%s"'
+ % (getattr(presentation, field_name), presentation._fullname),
+ locator=presentation._get_child_locator(field_name),
+ level=Issue.BETWEEN_TYPES)
+
+
+def report_issue_for_circular_type_hierarchy(context, presentation, field_name):
+ context.validation.report('"%s" of "%s" creates a circular type hierarchy'
+ % (getattr(presentation, field_name), presentation._fullname),
+ locator=presentation._get_child_locator(field_name),
+ level=Issue.BETWEEN_TYPES)
http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/8ee1470e/aria/parser/reading/__init__.py
----------------------------------------------------------------------
diff --git a/aria/parser/reading/__init__.py b/aria/parser/reading/__init__.py
new file mode 100644
index 0000000..32aa5b5
--- /dev/null
+++ b/aria/parser/reading/__init__.py
@@ -0,0 +1,39 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from .raw import RawReader
+from .reader import Reader
+from .yaml import YamlReader
+from .locator import Locator
+from .json import JsonReader
+from .jinja import JinjaReader
+from .context import ReadingContext
+from .source import ReaderSource, DefaultReaderSource
+from .exceptions import (ReaderException,
+ ReaderNotFoundError,
+ ReaderSyntaxError,
+ AlreadyReadException)
+
+__all__ = (
+ 'ReaderException',
+ 'ReaderNotFoundError',
+ 'ReaderSyntaxError',
+ 'AlreadyReadException',
+ 'Reader',
+ 'ReaderSource',
+ 'DefaultReaderSource',
+ 'ReadingContext',
+ 'RawReader',
+ 'Locator',
+ 'YamlReader',
+ 'JsonReader',
+ 'JinjaReader')
http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/8ee1470e/aria/parser/reading/context.py
----------------------------------------------------------------------
diff --git a/aria/parser/reading/context.py b/aria/parser/reading/context.py
new file mode 100644
index 0000000..81135dc
--- /dev/null
+++ b/aria/parser/reading/context.py
@@ -0,0 +1,29 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from .source import DefaultReaderSource
+from ..utils import LockedList
+
+
+class ReadingContext(object):
+ """
+ Properties:
+
+ * :code:`reader_source`: For finding reader instances
+ * :code:`reader`: Overrides :code:`reader_source` with a specific class
+ """
+
+ def __init__(self):
+ self.reader_source = DefaultReaderSource()
+ self.reader = None
+
+ self._locations = LockedList() # for keeping track of locations already read
http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/8ee1470e/aria/parser/reading/exceptions.py
----------------------------------------------------------------------
diff --git a/aria/parser/reading/exceptions.py b/aria/parser/reading/exceptions.py
new file mode 100644
index 0000000..bc2d2d5
--- /dev/null
+++ b/aria/parser/reading/exceptions.py
@@ -0,0 +1,44 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from .. import AriaException
+from ..validation import Issue
+
+
+class ReaderException(AriaException):
+ """
+ ARIA reader exception.
+ """
+
+
+class ReaderNotFoundError(ReaderException):
+ """
+ ARIA reader error: reader not found for source.
+ """
+
+
+class ReaderSyntaxError(ReaderException):
+ """
+ ARIA read format error.
+ """
+
+ def __init__(self, message, cause=None, cause_tb=None, location=None, line=None,
+ column=None, locator=None, snippet=None, level=Issue.SYNTAX):
+ super(ReaderSyntaxError, self).__init__(message, cause, cause_tb)
+ self.issue = Issue(message, location=location, line=line, column=column,
+ locator=locator, snippet=snippet, level=level)
+
+
+class AlreadyReadException(ReaderException):
+ """
+ ARIA reader exception: already read.
+ """
http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/8ee1470e/aria/parser/reading/jinja.py
----------------------------------------------------------------------
diff --git a/aria/parser/reading/jinja.py b/aria/parser/reading/jinja.py
new file mode 100644
index 0000000..e15e54e
--- /dev/null
+++ b/aria/parser/reading/jinja.py
@@ -0,0 +1,55 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+
+from jinja2 import Template
+
+from .. import VERSION
+from ..loading import LiteralLocation, LiteralLoader
+from .reader import Reader
+from .exceptions import ReaderSyntaxError
+
+
+# TODO: we could put a lot of other useful stuff here.
+CONTEXT = {
+ 'ARIA_VERSION': VERSION,
+ 'ENV': os.environ}
+
+
+class JinjaReader(Reader):
+ """
+ ARIA Jinja reader.
+
+ Forwards the rendered result to a new reader in the reader source.
+ """
+
+ def read(self):
+ data = self.load()
+ try:
+ data = str(data)
+ template = Template(data)
+ literal = template.render(CONTEXT)
+ # TODO: might be useful to write the literal result to a file for debugging
+ location = self.location
+ if isinstance(location, basestring) and location.endswith('.jinja'):
+ # Use reader based on the location with the ".jinja" prefix stripped off
+ location = location[:-6]
+ next_reader = self.context.reading.reader_source.get_reader(
+ self.context, LiteralLocation(literal, name=location), LiteralLoader(literal))
+ else:
+ # Use reader for literal loader
+ next_reader = self.context.reading.reader_source.get_reader(
+ self.context, LiteralLocation(literal), LiteralLoader(literal))
+ return next_reader.read()
+ except Exception as e:
+ raise ReaderSyntaxError('Jinja: %s' % e, cause=e)
http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/8ee1470e/aria/parser/reading/json.py
----------------------------------------------------------------------
diff --git a/aria/parser/reading/json.py b/aria/parser/reading/json.py
new file mode 100644
index 0000000..4747651
--- /dev/null
+++ b/aria/parser/reading/json.py
@@ -0,0 +1,33 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import absolute_import # so we can import standard 'json'
+
+import json
+from collections import OrderedDict
+
+from .reader import Reader
+from .exceptions import ReaderSyntaxError
+
+
+class JsonReader(Reader):
+ """
+ ARIA JSON reader.
+ """
+
+ def read(self):
+ data = self.load()
+ try:
+ data = unicode(data)
+ return json.loads(data, object_pairs_hook=OrderedDict)
+ except Exception as e:
+ raise ReaderSyntaxError('JSON: %s' % e, cause=e)
http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/8ee1470e/aria/parser/reading/locator.py
----------------------------------------------------------------------
diff --git a/aria/parser/reading/locator.py b/aria/parser/reading/locator.py
new file mode 100644
index 0000000..90b9e73
--- /dev/null
+++ b/aria/parser/reading/locator.py
@@ -0,0 +1,119 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from ..utils import puts, Colored, indent
+
+# We are inheriting the primitive types in order to add the ability to set
+# an attribute (_locator) on them.
+
+
+class LocatableString(unicode):
+ pass
+
+
+class LocatableInt(int):
+ pass
+
+
+class LocatableFloat(float):
+ pass
+
+
+def wrap(value):
+ if isinstance(value, basestring):
+ return True, LocatableString(value)
+ elif isinstance(value, int) and \
+ not isinstance(value, bool): # Note: bool counts as int in Python!
+ return True, LocatableInt(value)
+ elif isinstance(value, float):
+ return True, LocatableFloat(value)
+ return False, value
+
+
+class Locator(object):
+ """
+ Stores location information (line and column numbers) for agnostic raw data.
+ """
+ def __init__(self, location, line, column, children=None):
+ self.location = location
+ self.line = line
+ self.column = column
+ self.children = children
+
+ def get_child(self, *names):
+ if (not names) or (not isinstance(self.children, dict)):
+ return self
+ name = names[0]
+ if name not in self.children:
+ return self
+ child = self.children[name]
+ return child.get_child(names[1:])
+
+ def link(self, raw, path=None):
+ if hasattr(raw, '_locator'):
+ # This can happen when we use anchors
+ return
+
+ try:
+ setattr(raw, '_locator', self)
+ except AttributeError:
+ return
+
+ if isinstance(raw, list):
+ for i, raw_element in enumerate(raw):
+ wrapped, raw_element = wrap(raw_element)
+ if wrapped:
+ raw[i] = raw_element
+ child_path = '%s.%d' % (path, i) if path else str(i)
+ try:
+ self.children[i].link(raw_element, child_path)
+ except KeyError:
+ raise ValueError('location map does not match agnostic raw data: %s' %
+ child_path)
+ elif isinstance(raw, dict):
+ for k, raw_element in raw.iteritems():
+ wrapped, raw_element = wrap(raw_element)
+ if wrapped:
+ raw[k] = raw_element
+ child_path = '%s.%s' % (path, k) if path else k
+ try:
+ self.children[k].link(raw_element, child_path)
+ except KeyError:
+ raise ValueError('location map does not match agnostic raw data: %s' %
+ child_path)
+
+ def merge(self, locator):
+ if isinstance(self.children, dict) and isinstance(locator.children, dict):
+ for k, loc in locator.children.iteritems():
+ if k in self.children:
+ self.children[k].merge(loc)
+ else:
+ self.children[k] = loc
+
+ def dump(self, key=None):
+ if key:
+ puts('%s "%s":%d:%d' %
+ (Colored.red(key), Colored.blue(self.location), self.line, self.column))
+ else:
+ puts('"%s":%d:%d' % (Colored.blue(self.location), self.line, self.column))
+ if isinstance(self.children, list):
+ with indent(2):
+ for loc in self.children:
+ loc.dump()
+ elif isinstance(self.children, dict):
+ with indent(2):
+ for k, loc in self.children.iteritems():
+ loc.dump(k)
+
+ def __str__(self):
+ # Should be in same format as Issue.locator_as_str
+ return '"%s":%d:%d' % (self.location, self.line, self.column)
http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/8ee1470e/aria/parser/reading/raw.py
----------------------------------------------------------------------
diff --git a/aria/parser/reading/raw.py b/aria/parser/reading/raw.py
new file mode 100644
index 0000000..ed980ac
--- /dev/null
+++ b/aria/parser/reading/raw.py
@@ -0,0 +1,24 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from .reader import Reader
+
+
+class RawReader(Reader):
+ """
+ ARIA raw reader.
+
+ Expects to receive agnostic raw data from the loader, and so does nothing to it.
+ """
+
+ def read(self):
+ return self.load()
http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/8ee1470e/aria/parser/reading/reader.py
----------------------------------------------------------------------
diff --git a/aria/parser/reading/reader.py b/aria/parser/reading/reader.py
new file mode 100644
index 0000000..3a50739
--- /dev/null
+++ b/aria/parser/reading/reader.py
@@ -0,0 +1,44 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from ..utils import OpenClose
+from .exceptions import ReaderException, AlreadyReadException
+
+
+class Reader(object):
+ """
+ Base class for ARIA readers.
+
+ Readers provide agnostic raw data by consuming :class:`aria.loading.Loader` instances.
+ """
+
+ def __init__(self, context, location, loader):
+ self.context = context
+ self.location = location
+ self.loader = loader
+
+ def load(self):
+ with OpenClose(self.loader) as loader:
+ if self.context is not None:
+ with self.context._locations:
+ for location in self.context._locations:
+ if location.is_equivalent(loader.location):
+ raise AlreadyReadException('already read: %s' % loader.location)
+ self.context._locations.append(loader.location)
+
+ data = loader.load()
+ if data is None:
+ raise ReaderException('loader did not provide data: %s' % loader)
+ return data
+
+ def read(self):
+ raise NotImplementedError
http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/8ee1470e/aria/parser/reading/source.py
----------------------------------------------------------------------
diff --git a/aria/parser/reading/source.py b/aria/parser/reading/source.py
new file mode 100644
index 0000000..6fff2f6
--- /dev/null
+++ b/aria/parser/reading/source.py
@@ -0,0 +1,59 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from ..loading import LiteralLocation, UriLocation
+from .yaml import YamlReader
+from .json import JsonReader
+from .jinja import JinjaReader
+from .exceptions import ReaderNotFoundError
+
+
+EXTENSIONS = {
+ '.yaml': YamlReader,
+ '.json': JsonReader,
+ '.jinja': JinjaReader}
+
+
+class ReaderSource(object):
+ """
+ Base class for ARIA reader sources.
+
+ Reader sources provide appropriate :class:`Reader` instances for locations.
+ """
+
+ @staticmethod
+ def get_reader(context, location, loader): # pylint: disable=unused-argument
+ raise ReaderNotFoundError('location: %s' % location)
+
+
+class DefaultReaderSource(ReaderSource):
+ """
+ The default ARIA reader source will generate a :class:`YamlReader` for
+ locations that end in ".yaml", a :class:`JsonReader` for locations that
+ end in ".json", and a :class:`JinjaReader` for locations that end in
+ ".jinja".
+ """
+
+ def __init__(self, literal_reader_class=YamlReader):
+ super(DefaultReaderSource, self).__init__()
+ self.literal_reader_class = literal_reader_class
+
+ def get_reader(self, context, location, loader):
+ if isinstance(location, LiteralLocation):
+ return self.literal_reader_class(context, location, loader)
+
+ elif isinstance(location, UriLocation):
+ for extension, reader_class in EXTENSIONS.iteritems():
+ if location.uri.endswith(extension):
+ return reader_class(context, location, loader)
+
+ return super(DefaultReaderSource, self).get_reader(context, location, loader)
http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/8ee1470e/aria/parser/reading/yaml.py
----------------------------------------------------------------------
diff --git a/aria/parser/reading/yaml.py b/aria/parser/reading/yaml.py
new file mode 100644
index 0000000..55bcbd6
--- /dev/null
+++ b/aria/parser/reading/yaml.py
@@ -0,0 +1,114 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from collections import OrderedDict
+
+from ruamel import yaml # @UnresolvedImport
+
+from .reader import Reader
+from .locator import Locator
+from .exceptions import ReaderSyntaxError
+from .locator import LocatableString, LocatableInt, LocatableFloat
+
+# Add our types to ruamel.yaml
+yaml.representer.RoundTripRepresenter.add_representer(
+ LocatableString, yaml.representer.RoundTripRepresenter.represent_unicode)
+yaml.representer.RoundTripRepresenter.add_representer(
+ LocatableInt, yaml.representer.RoundTripRepresenter.represent_int)
+yaml.representer.RoundTripRepresenter.add_representer(
+ LocatableFloat, yaml.representer.RoundTripRepresenter.represent_float)
+
+MERGE_TAG = u'tag:yaml.org,2002:merge'
+MAP_TAG = u'tag:yaml.org,2002:map'
+
+
+class YamlLocator(Locator):
+ """
+ Map for agnostic raw data read from YAML.
+ """
+
+ def add_children(self, node):
+ if isinstance(node, yaml.SequenceNode):
+ self.children = []
+ for child_node in node.value:
+ self.add_child(child_node)
+ elif isinstance(node, yaml.MappingNode):
+ self.children = {}
+ for k, child_node in node.value:
+ self.add_child(child_node, k)
+
+ def add_child(self, node, key=None):
+ locator = YamlLocator(self.location, node.start_mark.line + 1, node.start_mark.column + 1)
+ if key is not None:
+ # Dict
+ if key.tag == MERGE_TAG:
+ for merge_key, merge_node in node.value:
+ self.add_child(merge_node, merge_key)
+ else:
+ self.children[key.value] = locator
+ else:
+ # List
+ self.children.append(locator)
+ locator.add_children(node)
+
+
+def construct_yaml_map(self, node):
+ data = OrderedDict()
+ yield data
+ value = self.construct_mapping(node)
+ data.update(value)
+
+
+yaml.constructor.SafeConstructor.add_constructor(MAP_TAG, construct_yaml_map)
+
+
+class YamlReader(Reader):
+ """
+ ARIA YAML reader.
+ """
+
+ def read(self):
+ data = self.load()
+ try:
+ data = unicode(data)
+ # see issue here:
+ # https://bitbucket.org/ruamel/yaml/issues/61/roundtriploader-causes-exceptions-with
+ #yaml_loader = yaml.RoundTripLoader(data)
+ yaml_loader = yaml.SafeLoader(data)
+ try:
+ node = yaml_loader.get_single_node()
+ locator = YamlLocator(self.loader.location, 0, 0)
+ if node is not None:
+ locator.add_children(node)
+ raw = yaml_loader.construct_document(node)
+ else:
+ raw = OrderedDict()
+ #locator.dump()
+ setattr(raw, '_locator', locator)
+ return raw
+ finally:
+ yaml_loader.dispose()
+ except yaml.parser.MarkedYAMLError as e:
+ context = e.context or 'while parsing'
+ problem = e.problem
+ line = e.problem_mark.line
+ column = e.problem_mark.column
+ snippet = e.problem_mark.get_snippet()
+ raise ReaderSyntaxError('YAML %s: %s %s' %
+ (e.__class__.__name__, problem, context),
+ location=self.loader.location,
+ line=line,
+ column=column,
+ snippet=snippet,
+ cause=e)
+ except Exception as e:
+ raise ReaderSyntaxError('YAML: %s' % e, cause=e)
http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/8ee1470e/aria/parser/specification.py
----------------------------------------------------------------------
diff --git a/aria/parser/specification.py b/aria/parser/specification.py
new file mode 100644
index 0000000..7a269f5
--- /dev/null
+++ b/aria/parser/specification.py
@@ -0,0 +1,79 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from collections import OrderedDict
+
+from .utils import full_type_name
+
+DSL_SPECIFICATION = {}
+DSL_SPECIFICATION_PACKAGES = []
+
+URL = {
+ 'tosca-simple-profile-1.0': 'http://docs.oasis-open.org/tosca/TOSCA-Simple-Profile-YAML/v1.0/'
+ 'csprd02/TOSCA-Simple-Profile-YAML-v1.0-csprd02.html',
+ 'tosca-simple-nfv-1.0': 'http://docs.oasis-open.org/tosca/tosca-nfv/v1.0/tosca-nfv-v1.0.html'}
+
+
+def dsl_specification(section, spec):
+ """
+ Decorator for TOSCA specification.
+
+ Used for documentation and standards compliance.
+ """
+
+ def decorator(obj):
+ specification = DSL_SPECIFICATION.get(spec)
+ if specification is None:
+ specification = {}
+ DSL_SPECIFICATION[spec] = specification
+ if section in specification:
+ raise Exception('you cannot specify the same @dsl_specification twice, consider adding'
+ ' \'-1\', \'-2\', etc.: %s, %s' % (spec, section))
+
+ url = URL.get(spec)
+ if url:
+ doc = obj.__doc__
+ if doc is not None:
+ url_start = doc.find(url)
+ if url_start != -1:
+ url_end = doc.find('>', url_start + len(url))
+ if url_end != -1:
+ url = doc[url_start:url_end]
+
+ specification[section] = OrderedDict((
+ ('code', full_type_name(obj)),
+ ('url', url)))
+ try:
+ setattr(obj, DSL_SPECIFICATION, {section: section, spec: spec})
+ except BaseException:
+ pass
+ return obj
+ return decorator
+
+
+def iter_spec(spec):
+ sections = DSL_SPECIFICATION[spec]
+ keys = sections.keys()
+ def key(value):
+ try:
+ parts = value.split('-', 1)
+ first = (int(v) for v in parts[0].split('.'))
+ second = parts[1] if len(parts) > 1 else None
+ return (first, second)
+ except ValueError:
+ return value
+ keys.sort(key=key)
+ for key in keys:
+ yield key, sections[key]
http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/8ee1470e/aria/parser/tools/__init__.py
----------------------------------------------------------------------
diff --git a/aria/parser/tools/__init__.py b/aria/parser/tools/__init__.py
new file mode 100644
index 0000000..ae1e83e
--- /dev/null
+++ b/aria/parser/tools/__init__.py
@@ -0,0 +1,14 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/8ee1470e/aria/parser/tools/cli.py
----------------------------------------------------------------------
diff --git a/aria/parser/tools/cli.py b/aria/parser/tools/cli.py
new file mode 100644
index 0000000..a274e49
--- /dev/null
+++ b/aria/parser/tools/cli.py
@@ -0,0 +1,69 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from aria.parser import install_aria_extensions
+from aria.parser.utils import (print_exception, import_fullname)
+from aria.parser.tools.utils import (CommonArgumentParser, create_context_from_namespace)
+from aria.parser.consumption import (ConsumerChain, Read, Validate, Model, Types, Inputs, Instance)
+
+class ArgumentParser(CommonArgumentParser):
+ def __init__(self):
+ super(ArgumentParser, self).__init__(description='CLI', prog='aria')
+ self.add_argument('uri', help='URI or file path to profile')
+ self.add_argument('consumer',
+ nargs='?',
+ default='instance',
+ help='consumer class name (full class path or short name)')
+
+def main():
+ try:
+
+ args, unknown_args = ArgumentParser().parse_known_args()
+
+ install_aria_extensions()
+
+ context = create_context_from_namespace(args)
+ context.args = unknown_args
+
+ consumer = ConsumerChain(context, (Read, Validate))
+
+ consumer_class_name = args.consumer
+ dumper = None
+ if consumer_class_name == 'presentation':
+ dumper = consumer.consumers[0]
+ elif consumer_class_name == 'model':
+ consumer.append(Model)
+ elif consumer_class_name == 'types':
+ consumer.append(Model, Types)
+ elif consumer_class_name == 'instance':
+ consumer.append(Model, Inputs, Instance)
+ else:
+ consumer.append(Model, Inputs, Instance)
+ consumer.append(import_fullname(consumer_class_name))
+
+ if dumper is None:
+ # Default to last consumer
+ dumper = consumer.consumers[-1]
+
+ consumer.consume()
+
+ if not context.validation.dump_issues():
+ dumper.dump()
+
+ except Exception as e:
+ print_exception(e)
+
+if __name__ == '__main__':
+ main()