| # 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 re |
| from datetime import (datetime, tzinfo, timedelta) |
| try: |
| from functools import total_ordering |
| except ImportError: |
| from total_ordering import total_ordering |
| |
| from aria.parser import implements_specification |
| from aria.utils.collections import (StrictDict, OrderedDict) |
| from aria.utils.formatting import safe_repr |
| |
| from .modeling.data_types import (coerce_to_data_type_class, report_issue_for_bad_format, |
| coerce_value) |
| |
| |
| class Timezone(tzinfo): |
| """ |
| Timezone as fixed offset in hours and minutes east of UTC. |
| """ |
| |
| def __init__(self, hours=0, minutes=0): |
| super(Timezone, self).__init__() |
| self._offset = timedelta(hours=hours, minutes=minutes) |
| |
| def utcoffset(self, dt): # pylint: disable=unused-argument |
| return self._offset |
| |
| def tzname(self, dt): # pylint: disable=unused-argument |
| return unicode(self._offset) |
| |
| def dst(self, dt): # pylint: disable=unused-argument |
| return Timezone._ZERO |
| |
| _ZERO = timedelta(0) |
| |
| |
| UTC = Timezone() |
| |
| |
| @total_ordering |
| @implements_specification('timestamp', 'yaml-1.1') |
| class Timestamp(object): |
| ''' |
| TOSCA timestamps follow the YAML specification, which in turn is a variant of ISO8601. |
| |
| Long forms and short forms (without time of day and assuming UTC timezone) are supported for |
| parsing. The canonical form (for rendering) matches the long form at the UTC timezone. |
| |
| See the `Timestamp Language-Independent Type for YAML Version 1.1 (Working Draft 2005-01-18) |
| <http://yaml.org/type/timestamp.html>`__ |
| ''' |
| |
| REGULAR_SHORT = r'^(?P<year>[0-9][0-9][0-9][0-9])-(?P<month>[0-9][0-9])-(?P<day>[0-9][0-9])$' |
| REGULAR_LONG = \ |
| r'^(?P<year>[0-9][0-9][0-9][0-9])-(?P<month>[0-9][0-9]?)-(?P<day>[0-9][0-9]?)' + \ |
| r'([Tt]|[ \t]+)' \ |
| r'(?P<hour>[0-9][0-9]?):(?P<minute>[0-9][0-9]):(?P<second>[0-9][0-9])' + \ |
| r'(?P<fraction>\.[0-9]*)?' + \ |
| r'(([ \t]*)Z|(?P<tzhour>[-+][0-9][0-9])?(:(?P<tzminute>[0-9][0-9])?)?)?$' |
| CANONICAL = '%Y-%m-%dT%H:%M:%S' |
| |
| def __init__(self, entry_schema, constraints, value, aspect): # pylint: disable=unused-argument |
| value = unicode(value) |
| match = re.match(Timestamp.REGULAR_SHORT, value) |
| if match is not None: |
| # Parse short form |
| year = int(match.group('year')) |
| month = int(match.group('month')) |
| day = int(match.group('day')) |
| self.value = datetime(year, month, day, tzinfo=UTC) |
| else: |
| match = re.match(Timestamp.REGULAR_LONG, value) |
| if match is not None: |
| # Parse long form |
| year = int(match.group('year')) |
| month = int(match.group('month')) |
| day = int(match.group('day')) |
| hour = match.group('hour') |
| if hour is not None: |
| hour = int(hour) |
| minute = match.group('minute') |
| if minute is not None: |
| minute = int(minute) |
| second = match.group('second') |
| if second is not None: |
| second = int(second) |
| fraction = match.group('fraction') |
| if fraction is not None: |
| fraction = int(float(fraction) * 1000000.0) # convert to microseconds |
| tzhour = match.group('tzhour') |
| if tzhour is not None: |
| tzhour = int(tzhour) |
| else: |
| tzhour = 0 |
| tzminute = match.group('tzminute') |
| if tzminute is not None: |
| tzminute = int(tzminute) |
| else: |
| tzminute = 0 |
| self.value = datetime(year, month, day, hour, minute, second, fraction, |
| Timezone(tzhour, tzminute)) |
| else: |
| raise ValueError( |
| u'timestamp must be formatted as YAML ISO8601 variant or "YYYY-MM-DD": {0}' |
| .format(safe_repr(value))) |
| |
| @property |
| def as_datetime_utc(self): |
| return self.value.astimezone(UTC) |
| |
| @property |
| def as_raw(self): |
| return self.__str__() |
| |
| def __str__(self): |
| the_datetime = self.as_datetime_utc |
| return u'{0}{1}Z'.format(the_datetime.strftime(Timestamp.CANONICAL), |
| Timestamp._fraction_as_str(the_datetime)) |
| |
| def __repr__(self): |
| return repr(self.__str__()) |
| |
| def __eq__(self, timestamp): |
| if not isinstance(timestamp, Timestamp): |
| return False |
| return self.value == timestamp.value |
| |
| def __lt__(self, timestamp): |
| return self.value < timestamp.value |
| |
| @staticmethod |
| def _fraction_as_str(the_datetime): |
| return u'{0:g}'.format(the_datetime.microsecond / 1000000.0).lstrip('0') |
| |
| |
| @total_ordering |
| @implements_specification('3.2.2', 'tosca-simple-1.0') |
| class Version(object): |
| """ |
| TOSCA supports the concept of "reuse" of type definitions, as well as template definitions which |
| could be version and change over time. It is important to provide a reliable, normative means to |
| represent a version string which enables the comparison and management of types and templates |
| over time. Therefore, the TOSCA TC intends to provide a normative version type (string) for this |
| purpose in future Working Drafts of this specification. |
| |
| See the `TOSCA Simple Profile v1.0 cos01 specification <http://docs.oasis-open.org/tosca |
| /TOSCA-Simple-Profile-YAML/v1.0/cos01/TOSCA-Simple-Profile-YAML-v1.0-cos01.html |
| #TYPE_TOSCA_VERSION>`__ |
| """ |
| |
| REGEX = \ |
| r'^(?P<major>\d+)\.(?P<minor>\d+)(\.(?P<fix>\d+)' + \ |
| r'((\.(?P<qualifier>\w+))(\-(?P<build>\d+))?)?)?$' |
| |
| @staticmethod |
| def key(version): |
| """ |
| Key method for fast sorting. |
| """ |
| return (version.major, version.minor, version.fix, version.qualifier, version.build) |
| |
| def __init__(self, entry_schema, constraints, value, aspect): # pylint: disable=unused-argument |
| str_value = unicode(value) |
| match = re.match(Version.REGEX, str_value, flags=re.UNICODE) |
| if match is None: |
| raise ValueError( |
| u'version must be formatted as <major_version>.<minor_version>' |
| u'[.<fix_version>[.<qualifier>[-<build_version]]]: {0}'.format(safe_repr(value))) |
| |
| self.value = str_value |
| |
| self.major = match.group('major') |
| self.major = int(self.major) |
| self.minor = match.group('minor') |
| self.minor = int(self.minor) |
| self.fix = match.group('fix') |
| if self.fix is not None: |
| self.fix = int(self.fix) |
| self.qualifier = match.group('qualifier') |
| self.build = match.group('build') |
| if self.build is not None: |
| self.build = int(self.build) |
| |
| @property |
| def as_raw(self): |
| return self.value |
| |
| def __str__(self): |
| return self.value |
| |
| def __repr__(self): |
| return repr(self.__str__()) |
| |
| def __eq__(self, version): |
| if not isinstance(version, Version): |
| return False |
| return (self.major, self.minor, self.fix, self.qualifier, self.build) == \ |
| (version.major, version.minor, version.fix, version.qualifier, version.build) |
| |
| @implements_specification('3.2.2.1', 'tosca-simple-1.0') |
| def __lt__(self, version): |
| if self.major < version.major: |
| return True |
| elif self.major == version.major: |
| if self.minor < version.minor: |
| return True |
| elif self.minor == version.minor: |
| if self.fix < version.fix: |
| return True |
| elif self.fix == version.fix: |
| if self.qualifier == version.qualifier: |
| if self.build < version.build: |
| return True |
| return False |
| |
| |
| @implements_specification('3.2.3', 'tosca-simple-1.0') |
| class Range(object): |
| """ |
| The range type can be used to define numeric ranges with a lower and upper boundary. For |
| example, this allows for specifying a range of ports to be opened in a firewall. |
| |
| See the `TOSCA Simple Profile v1.0 cos01 specification <http://docs.oasis-open.org/tosca |
| /TOSCA-Simple-Profile-YAML/v1.0/cos01/TOSCA-Simple-Profile-YAML-v1.0-cos01.html |
| #TYPE_TOSCA_RANGE>`__ |
| """ |
| |
| def __init__(self, entry_schema, constraints, value, aspect): # pylint: disable=unused-argument |
| if not isinstance(value, list): |
| raise ValueError(u'range value is not a list: {0}'.format(safe_repr(value))) |
| if len(value) != 2: |
| raise ValueError(u'range value does not have exactly 2 elements: {0}' |
| .format(safe_repr(value))) |
| |
| def is_int(v): |
| return isinstance(v, int) and (not isinstance(v, bool)) # In Python bool is an int |
| |
| if not is_int(value[0]): |
| raise ValueError(u'lower bound of range is not a valid integer: {0}' |
| .format(safe_repr(value[0]))) |
| |
| if value[1] != 'UNBOUNDED': |
| if not is_int(value[1]): |
| raise ValueError(u'upper bound of range is not a valid integer or "UNBOUNDED": {0}' |
| .format(safe_repr(value[0]))) |
| |
| if value[0] >= value[1]: |
| raise ValueError( |
| u'upper bound of range is not greater than the lower bound: {0} >= {1}' |
| .format(safe_repr(value[0]), safe_repr(value[1]))) |
| |
| self.value = value |
| |
| def is_in(self, value): |
| if value < self.value[0]: |
| return False |
| if (self.value[1] != 'UNBOUNDED') and (value > self.value[1]): |
| return False |
| return True |
| |
| @property |
| def as_raw(self): |
| return list(self.value) |
| |
| |
| @implements_specification('3.2.4', 'tosca-simple-1.0') |
| class List(list): |
| """ |
| The list type allows for specifying multiple values for a parameter of property. For example, if |
| an application allows for being configured to listen on multiple ports, a list of ports could be |
| configured using the list data type. |
| |
| See the `TOSCA Simple Profile v1.0 cos01 specification <http://docs.oasis-open.org/tosca |
| /TOSCA-Simple-Profile-YAML/v1.0/cos01/TOSCA-Simple-Profile-YAML-v1.0-cos01.html |
| #TYPE_TOSCA_LIST>`__ |
| """ |
| |
| @staticmethod |
| def _create(context, presentation, entry_schema, constraints, value, aspect): # pylint: disable=unused-argument |
| if not isinstance(value, list): |
| raise ValueError(u'"list" data type value is not a list: {0}'.format(safe_repr(value))) |
| |
| entry_schema_type = entry_schema._get_type(context) |
| entry_schema_constraints = entry_schema.constraints |
| |
| the_list = List() |
| for v in value: |
| v = coerce_value(context, presentation, entry_schema_type, None, |
| entry_schema_constraints, v, aspect) |
| if v is not None: |
| the_list.append(v) |
| |
| return the_list |
| |
| # Can't define as property because it's old-style Python class |
| def as_raw(self): |
| return list(self) |
| |
| |
| @implements_specification('3.2.5', 'tosca-simple-1.0') |
| class Map(StrictDict): |
| """ |
| The map type allows for specifying multiple values for a parameter of property as a map. In |
| contrast to the list type, where each entry can only be addressed by its index in the list, |
| entries in a map are named elements that can be addressed by their keys. |
| |
| See the `TOSCA Simple Profile v1.0 cos01 specification <http://docs.oasis-open.org/tosca |
| /TOSCA-Simple-Profile-YAML/v1.0/cos01/TOSCA-Simple-Profile-YAML-v1.0-cos01.html |
| #TYPE_TOSCA_MAP>`__ |
| """ |
| |
| @staticmethod |
| def _create(context, presentation, entry_schema, constraints, value, aspect): # pylint: disable=unused-argument |
| if not isinstance(value, dict): |
| raise ValueError(u'"map" data type value is not a dict: {0}'.format(safe_repr(value))) |
| |
| if entry_schema is None: |
| raise ValueError(u'"map" data type does not define "entry_schema"') |
| |
| entry_schema_type = entry_schema._get_type(context) |
| entry_schema_constraints = entry_schema.constraints |
| |
| the_map = Map() |
| for k, v in value.iteritems(): |
| v = coerce_value(context, presentation, entry_schema_type, None, |
| entry_schema_constraints, v, aspect) |
| if v is not None: |
| the_map[k] = v |
| |
| return the_map |
| |
| def __init__(self, items=None): |
| super(Map, self).__init__(items, key_class=basestring) |
| |
| # Can't define as property because it's old-style Python class |
| def as_raw(self): |
| return OrderedDict(self) |
| |
| |
| @total_ordering |
| @implements_specification('3.2.6', 'tosca-simple-1.0') |
| class Scalar(object): |
| """ |
| The scalar-unit type can be used to define scalar values along with a unit from the list of |
| recognized units. |
| |
| See the `TOSCA Simple Profile v1.0 cos01 specification <http://docs.oasis-open.org/tosca |
| /TOSCA-Simple-Profile-YAML/v1.0/cos01/TOSCA-Simple-Profile-YAML-v1.0-cos01.html |
| #TYPE_TOSCA_SCALAR_UNIT>`__ |
| """ |
| |
| @staticmethod |
| def key(scalar): |
| """ |
| Key method for fast sorting. |
| """ |
| return scalar.value |
| |
| def __init__(self, entry_schema, constraints, value, aspect): # pylint: disable=unused-argument |
| str_value = unicode(value) |
| match = re.match(self.REGEX, str_value, flags=re.UNICODE) # pylint: disable=no-member |
| if match is None: |
| raise ValueError(u'scalar must be formatted as <scalar> <unit>: {0}' |
| .format(safe_repr(value))) |
| |
| self.factor = float(match.group('scalar')) |
| if self.factor < 0: |
| raise ValueError('scalar is negative: {0}'.format(safe_repr(self.factor))) |
| |
| self.unit = match.group('unit') |
| |
| unit_lower = self.unit.lower() |
| unit_size = None |
| for k, v in self.UNITS.iteritems(): # pylint: disable=no-member |
| if k.lower() == unit_lower: |
| self.unit = k |
| unit_size = v |
| break |
| if unit_size is None: |
| raise ValueError(u'scalar specified with unsupported unit: {0}' |
| .format(safe_repr(self.unit))) |
| |
| self.value = self.TYPE(self.factor * unit_size) # pylint: disable=no-member |
| |
| @property |
| def as_raw(self): |
| return OrderedDict(( |
| ('value', self.value), |
| ('factor', self.factor), |
| ('unit', self.unit), |
| ('unit_size', self.UNITS[self.unit]))) # pylint: disable=no-member |
| |
| def __str__(self): |
| return u'{0} {1}'.format(self.value, self.UNIT) # pylint: disable=no-member |
| |
| def __repr__(self): |
| return repr(self.__str__()) |
| |
| def __eq__(self, scalar): |
| if isinstance(scalar, Scalar): |
| value = scalar.value |
| else: |
| value = self.TYPE(scalar) # pylint: disable=no-member |
| return self.value == value |
| |
| def __lt__(self, scalar): |
| if isinstance(scalar, Scalar): |
| value = scalar.value |
| else: |
| value = self.TYPE(scalar) # pylint: disable=no-member |
| return self.value < value |
| |
| |
| @implements_specification('3.2.6.4', 'tosca-simple-1.0') |
| class ScalarSize(Scalar): |
| """ |
| Integer scalar for counting bytes. |
| |
| See the `TOSCA Simple Profile v1.0 cos01 specification <http://docs.oasis-open.org/tosca |
| /TOSCA-Simple-Profile-YAML/v1.0/cos01/TOSCA-Simple-Profile-YAML-v1.0-cos01.html |
| #TYPE_TOSCA_SCALAR_UNIT_SIZE>`__ |
| """ |
| |
| # See: http://www.regular-expressions.info/floatingpoint.html |
| REGEX = \ |
| r'^(?P<scalar>[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)\s*(?P<unit>B|kB|KiB|MB|MiB|GB|GiB|TB|TiB)$' |
| |
| UNITS = { |
| 'B': 1, |
| 'kB': 1000, |
| 'KiB': 1024, |
| 'MB': 1000000, |
| 'MiB': 1048576, |
| 'GB': 1000000000, |
| 'GiB': 1073741824, |
| 'TB': 1000000000000, |
| 'TiB': 1099511627776} |
| |
| TYPE = int |
| UNIT = 'bytes' |
| |
| |
| @implements_specification('3.2.6.5', 'tosca-simple-1.0') |
| class ScalarTime(Scalar): |
| """ |
| Floating point scalar for counting seconds. |
| |
| See the `TOSCA Simple Profile v1.0 cos01 specification <http://docs.oasis-open.org/tosca |
| /TOSCA-Simple-Profile-YAML/v1.0/cos01/TOSCA-Simple-Profile-YAML-v1.0-cos01.html |
| #TYPE_TOSCA_SCALAR_UNIT_TIME>`__ |
| """ |
| |
| # See: http://www.regular-expressions.info/floatingpoint.html |
| REGEX = r'^(?P<scalar>[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)\s*(?P<unit>ns|us|ms|s|m|h|d)$' |
| |
| UNITS = { |
| 'ns': 0.000000001, |
| 'us': 0.000001, |
| 'ms': 0.001, |
| 's': 1.0, |
| 'm': 60.0, |
| 'h': 3600.0, |
| 'd': 86400.0} |
| |
| TYPE = float |
| UNIT = 'seconds' |
| |
| |
| @implements_specification('3.2.6.6', 'tosca-simple-1.0') |
| class ScalarFrequency(Scalar): |
| """ |
| Floating point scalar for counting cycles per second (Hz). |
| |
| See the `TOSCA Simple Profile v1.0 cos01 specification <http://docs.oasis-open.org/tosca |
| /TOSCA-Simple-Profile-YAML/v1.0/cos01/TOSCA-Simple-Profile-YAML-v1.0-cos01.html |
| #TYPE_TOSCA_SCALAR_UNIT_FREQUENCY>`__ |
| """ |
| |
| # See: http://www.regular-expressions.info/floatingpoint.html |
| REGEX = r'^(?P<scalar>[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)\s*(?P<unit>Hz|kHz|MHz|GHz)$' |
| |
| UNITS = { |
| 'Hz': 1.0, |
| 'kHz': 1000.0, |
| 'MHz': 1000000.0, |
| 'GHz': 1000000000.0} |
| |
| TYPE = float |
| UNIT = 'Hz' |
| |
| |
| # |
| # The following are hooked in the YAML as 'coerce_value' extensions |
| # |
| |
| def coerce_timestamp(context, presentation, the_type, entry_schema, constraints, value, aspect): # pylint: disable=unused-argument |
| return coerce_to_data_type_class(context, presentation, Timestamp, entry_schema, constraints, |
| value, aspect) |
| |
| |
| def coerce_version(context, presentation, the_type, entry_schema, constraints, value, aspect): # pylint: disable=unused-argument |
| return coerce_to_data_type_class(context, presentation, Version, entry_schema, constraints, |
| value, aspect) |
| |
| |
| def coerce_range(context, presentation, the_type, entry_schema, constraints, value, aspect): |
| if aspect == 'in_range': |
| # When we're in a "in_range" constraint, the values are *not* themselves ranges, but numbers |
| try: |
| return float(value) |
| except ValueError as e: |
| report_issue_for_bad_format(context, presentation, the_type, value, aspect, e) |
| except TypeError as e: |
| report_issue_for_bad_format(context, presentation, the_type, value, aspect, e) |
| else: |
| return coerce_to_data_type_class(context, presentation, Range, entry_schema, constraints, |
| value, aspect) |
| |
| |
| def coerce_list(context, presentation, the_type, entry_schema, constraints, value, aspect): # pylint: disable=unused-argument |
| return coerce_to_data_type_class(context, presentation, List, entry_schema, constraints, |
| value, aspect) |
| |
| |
| def coerce_map_value(context, presentation, the_type, entry_schema, constraints, value, aspect): # pylint: disable=unused-argument |
| return coerce_to_data_type_class(context, presentation, Map, entry_schema, constraints, value, |
| aspect) |
| |
| |
| def coerce_scalar_unit_size(context, presentation, the_type, entry_schema, constraints, value, # pylint: disable=unused-argument |
| aspect): |
| return coerce_to_data_type_class(context, presentation, ScalarSize, entry_schema, constraints, |
| value, aspect) |
| |
| |
| def coerce_scalar_unit_time(context, presentation, the_type, entry_schema, constraints, value, # pylint: disable=unused-argument |
| aspect): |
| return coerce_to_data_type_class(context, presentation, ScalarTime, entry_schema, constraints, |
| value, aspect) |
| |
| |
| def coerce_scalar_unit_frequency(context, presentation, the_type, entry_schema, constraints, value, # pylint: disable=unused-argument |
| aspect): |
| return coerce_to_data_type_class(context, presentation, ScalarFrequency, entry_schema, |
| constraints, value, aspect) |