예제 #1
0
class Amount(Base):
    """
    Conformity field that ensures that the value is an instance of `currint.Amount` and optionally enforces boundaries
    for that amount with the `valid_currencies`, `gt`, `gte`, `lt`, and `lte` arguments. This field requires that
    Currint be installed.
    """

    introspect_type = 'currint.Amount'

    valid_currencies = attr.ib(
        default=frozenset(),
        validator=attr_is_iterable(attr_is_string(), attr_is_set()),
    )  # type: AbstractSet[six.text_type]
    gt = attr.ib(default=None, validator=attr_is_optional(attr_is_int()))  # type: Optional[int]
    gte = attr.ib(default=None, validator=attr_is_optional(attr_is_int()))  # type: Optional[int]
    lt = attr.ib(default=None, validator=attr_is_optional(attr_is_int()))  # type: Optional[int]
    lte = attr.ib(default=None, validator=attr_is_optional(attr_is_int()))  # type: Optional[int]
    description = attr.ib(default=None, validator=attr_is_optional(attr_is_string()))  # type: Optional[six.text_type]

    def __attrs_post_init__(self):  # type: () -> None
        if not self.valid_currencies:
            self.valid_currencies = DEFAULT_CURRENCY_CODES

    def errors(self, value):  # type: (AnyType) -> ListType[Error]
        if not isinstance(value, currint.Amount):
            return [Error(
                'Not a currint.Amount instance',
                code=ERROR_CODE_INVALID,
            )]

        return _get_errors_for_currency_amount(
            value.currency.code,
            value.value,
            self.valid_currencies,
            self.gt,
            self.gte,
            self.lt,
            self.lte,
        )

    def introspect(self):  # type: () -> Introspection
        return strip_none({
            'type': self.introspect_type,
            'description': self.description,
            'valid_currencies': (
                '(all currencies)' if self.valid_currencies is DEFAULT_CURRENCY_CODES else sorted(self.valid_currencies)
            ),
            'gt': self.gt,
            'gte': self.gte,
            'lt': self.lt,
            'lte': self.lte,
        })
예제 #2
0
class Dictionary(Base):
    """
    Conformity field that ensures that the value is a dictionary with a specific set of keys and value that validate
    with the Conformity fields associated with those keys (`contents`). Keys are required unless they are listed in
    the `optional_keys` argument. No extra keys are allowed unless the `allow_extra_keys` argument is set to `True`.

    If the `contents` argument is an instance of `OrderedDict`, the field introspection will include a `display_order`
    list of keys matching the order they exist in the `OrderedDict`, and errors will be reported in the order the keys
    exist in the `OrderedDict`. Order will be maintained for any calls to `extend` as long as those calls also use
    `OrderedDict`. Ordering behavior is undefined otherwise. This field does NOT enforce that the value it validates
    presents keys in the same order. `OrderedDict` is used strictly for documentation and error-object-ordering
    purposes only.
    """

    introspect_type = 'dictionary'

    # Makes MyPy allow optional_keys to have this type
    _optional_keys_default = frozenset(
    )  # type: Union[TupleType[HashableType, ...], FrozenSet[HashableType]]

    contents = attr.ib(
        default=None,
        validator=attr_is_optional(attr_is_instance(dict)),
    )  # type: Mapping[HashableType, Base]
    optional_keys = attr.ib(
        default=_optional_keys_default,
        validator=attr_is_iterable(attr_is_instance(object)),
    )  # type: Union[TupleType[HashableType, ...], FrozenSet[HashableType]]
    allow_extra_keys = attr.ib(default=None)  # type: bool
    description = attr.ib(
        default=None, validator=attr_is_optional(
            attr_is_string()))  # type: Optional[six.text_type]
    additional_validator = attr.ib(
        default=None,
        validator=attr_is_optional(
            attr_is_instance(AdditionalCollectionValidator)),
    )  # type: Optional[AdditionalCollectionValidator[Mapping[HashableType, AnyType]]]

    def __attrs_post_init__(self):  # type: () -> None
        if self.contents is None and getattr(self.__class__, 'contents',
                                             None) is not None:
            # If no contents were provided but a subclass has hard-coded contents, use those
            self.contents = self.__class__.contents
        if self.contents is None:
            # If there are still no contents, raise an error
            raise ValueError("'contents' is a required argument")
        if not isinstance(self.contents, dict):
            raise TypeError("'contents' must be a dict")

        if (self.optional_keys is self._optional_keys_default and getattr(
                self.__class__, 'optional_keys', None) is not None):
            # If the optional_keys argument was defaulted (not specified) but a subclass has it hard-coded, use that
            self.optional_keys = self.__class__.optional_keys
        if not isinstance(self.optional_keys, frozenset):
            self.optional_keys = frozenset(self.optional_keys)

        if self.allow_extra_keys is None and getattr(
                self.__class__, 'allow_extra_keys', None) is not None:
            # If the allow_extra_keys argument was not specified but a subclass has it hard-coded, use that value
            self.allow_extra_keys = self.__class__.allow_extra_keys
        if self.allow_extra_keys is None:
            # If no value is found, default to False
            self.allow_extra_keys = False
        if not isinstance(self.allow_extra_keys, bool):
            raise TypeError("'allow_extra_keys' must be a boolean")

        if self.description is None and getattr(self.__class__, 'description',
                                                None):
            # If the description was not specified but a subclass has it hard-coded, use that value
            self.description = self.__class__.description
        if self.description is not None and not isinstance(
                self.description, six.text_type):
            raise TypeError("'description' must be a unicode string")

    def errors(self, value):  # type: (AnyType) -> ListType[Error]
        if not isinstance(value, dict):
            return [Error('Not a dict')]

        result = []
        for key, field in self.contents.items():
            # Check key is present
            if key not in value:
                if key not in self.optional_keys:
                    result.append(
                        Error('Missing key: {}'.format(key),
                              code=ERROR_CODE_MISSING,
                              pointer=six.text_type(key)), )
            else:
                # Check key type
                result.extend(
                    update_error_pointer(error, key)
                    for error in (field.errors(value[key]) or []))
        # Check for extra keys
        extra_keys = set(value.keys()) - set(self.contents.keys())
        if extra_keys and not self.allow_extra_keys:
            result.append(
                Error(
                    'Extra keys present: {}'.format(', '.join(
                        six.text_type(key) for key in sorted(extra_keys))),
                    code=ERROR_CODE_UNKNOWN,
                ), )

        if not result and self.additional_validator:
            return self.additional_validator.errors(value)

        return result

    def extend(
        self,
        contents=None,  # type: Optional[Mapping[HashableType, Base]]
        optional_keys=None,  # type: Optional[Union[TupleType[HashableType, ...], FrozenSet[HashableType]]]
        allow_extra_keys=None,  # type: Optional[bool]
        description=None,  # type: Optional[six.text_type]
        replace_optional_keys=False,  # type: bool
        additional_validator=None,  # type: Optional[AdditionalCollectionValidator[Mapping[HashableType, AnyType]]]
    ):
        # type: (...) -> Dictionary
        """
        This method allows you to create a new `Dictionary` that extends the current `Dictionary` with additional
        contents and/or optional keys, and/or replaces the `allow_extra_keys` and/or `description` attributes.

        :param contents: More contents, if any, to extend the current contents
        :param optional_keys: More optional keys, if any, to extend the current optional keys
        :param allow_extra_keys: If non-`None`, this overrides the current `allow_extra_keys` attribute
        :param description: If non-`None`, this overrides the current `description` attribute
        :param replace_optional_keys: If `True`, then the `optional_keys` argument will completely replace, instead of
                                      extend, the current optional keys
        :param additional_validator: If non-`None`, this overrides the current `additional_validator` attribute

        :return: A new `Dictionary` extended from the current `Dictionary` based on the supplied arguments
        """
        optional_keys = frozenset(optional_keys or ())
        return Dictionary(
            contents=cast(Type[Union[Dict, OrderedDict]], type(self.contents))(
                (k, v) for d in (self.contents, contents)
                for k, v in six.iteritems(d)) if contents else self.contents,
            optional_keys=optional_keys if replace_optional_keys else
            frozenset(self.optional_keys) | optional_keys,
            allow_extra_keys=self.allow_extra_keys
            if allow_extra_keys is None else allow_extra_keys,
            description=self.description
            if description is None else description,
            additional_validator=self.additional_validator
            if additional_validator is None else additional_validator,
        )

    def introspect(self):  # type: () -> Introspection
        display_order = None  # type: Optional[ListType[AnyType]]
        if isinstance(self.contents, OrderedDict):
            display_order = list(self.contents.keys())

        return strip_none({
            'type':
            self.introspect_type,
            'contents':
            {key: value.introspect()
             for key, value in self.contents.items()},
            'optional_keys':
            sorted(self.optional_keys),
            'allow_extra_keys':
            self.allow_extra_keys,
            'description':
            self.description,
            'display_order':
            display_order,
            'additional_validation':
            (self.additional_validator.__class__.__name__
             if self.additional_validator else None),
        })
예제 #3
0
class AmountString(Base):
    """
    Conformity field that ensures that the value is a unicode string matching the format CUR,1234 or CUR:1234, where
    the part before the delimiter is a valid currency and the part after the delimiter is an integer. It also optionally
    enforces boundaries for those values with the `valid_currencies`, `gt`, `gte`, `lt`, and `lte` arguments. This
    field requires that Currint be installed.
    """

    _format = re.compile(r'[,:]')

    introspect_type = 'currency_amount_string'

    valid_currencies = attr.ib(
        default=frozenset(),
        validator=attr_is_iterable(attr_is_string(), attr_is_set()),
    )  # type: AbstractSet[six.text_type]
    gt = attr.ib(default=None, validator=attr_is_optional(
        attr_is_int()))  # type: Optional[int]
    gte = attr.ib(default=None, validator=attr_is_optional(
        attr_is_int()))  # type: Optional[int]
    lt = attr.ib(default=None, validator=attr_is_optional(
        attr_is_int()))  # type: Optional[int]
    lte = attr.ib(default=None, validator=attr_is_optional(
        attr_is_int()))  # type: Optional[int]
    description = attr.ib(
        default=None, validator=attr_is_optional(
            attr_is_string()))  # type: Optional[six.text_type]

    def __attrs_post_init__(self):  # type: () -> None
        if not self.valid_currencies:
            self.valid_currencies = DEFAULT_CURRENCY_CODES

    def errors(self, value):  # type: (AnyType) -> ListType[Error]
        if not isinstance(value, six.text_type):
            return [Error('Not a unicode string currency amount')]

        parts = self._format.split(value)
        if len(parts) != 2:
            return [
                Error(
                    'Currency string does not match format CUR,1234 or CUR:1234'
                )
            ]

        currency = parts[0]
        try:
            value = int(parts[1])
        except ValueError:
            return [
                Error('Currency amount {} cannot be converted to an integer'.
                      format(parts[1]))
            ]

        return _get_errors_for_currency_amount(
            currency,
            value,
            self.valid_currencies,
            self.gt,
            self.gte,
            self.lt,
            self.lte,
        )

    def introspect(self):  # type: () -> Introspection
        return strip_none({
            'type':
            self.introspect_type,
            'description':
            self.description,
            'valid_currencies':
            ('(all currencies)'
             if self.valid_currencies is DEFAULT_CURRENCY_CODES else sorted(
                 self.valid_currencies)),
            'gt':
            self.gt,
            'gte':
            self.gte,
            'lt':
            self.lt,
            'lte':
            self.lte,
        })