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, })
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), })
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, })