D7net
Home
Console
Upload
information
Create File
Create Folder
About
Tools
:
/
proc
/
self
/
root
/
opt
/
cloudlinux
/
venv
/
lib
/
python3.11
/
site-packages
/
pydantic
/
v1
/
Filename :
types.py
back
Copy
import abc import math import re import warnings from datetime import date from decimal import Decimal, InvalidOperation from enum import Enum from pathlib import Path from types import new_class from typing import ( TYPE_CHECKING, Any, Callable, ClassVar, Dict, FrozenSet, List, Optional, Pattern, Set, Tuple, Type, TypeVar, Union, cast, overload, ) from uuid import UUID from weakref import WeakSet from . import errors from .datetime_parse import parse_date from .utils import import_string, update_not_none from .validators import ( bytes_validator, constr_length_validator, constr_lower, constr_strip_whitespace, constr_upper, decimal_validator, float_finite_validator, float_validator, frozenset_validator, int_validator, list_validator, number_multiple_validator, number_size_validator, path_exists_validator, path_validator, set_validator, str_validator, strict_bytes_validator, strict_float_validator, strict_int_validator, strict_str_validator, ) __all__ = [ 'NoneStr', 'NoneBytes', 'StrBytes', 'NoneStrBytes', 'StrictStr', 'ConstrainedBytes', 'conbytes', 'ConstrainedList', 'conlist', 'ConstrainedSet', 'conset', 'ConstrainedFrozenSet', 'confrozenset', 'ConstrainedStr', 'constr', 'PyObject', 'ConstrainedInt', 'conint', 'PositiveInt', 'NegativeInt', 'NonNegativeInt', 'NonPositiveInt', 'ConstrainedFloat', 'confloat', 'PositiveFloat', 'NegativeFloat', 'NonNegativeFloat', 'NonPositiveFloat', 'FiniteFloat', 'ConstrainedDecimal', 'condecimal', 'UUID1', 'UUID3', 'UUID4', 'UUID5', 'FilePath', 'DirectoryPath', 'Json', 'JsonWrapper', 'SecretField', 'SecretStr', 'SecretBytes', 'StrictBool', 'StrictBytes', 'StrictInt', 'StrictFloat', 'PaymentCardNumber', 'ByteSize', 'PastDate', 'FutureDate', 'ConstrainedDate', 'condate', ] NoneStr = Optional[str] NoneBytes = Optional[bytes] StrBytes = Union[str, bytes] NoneStrBytes = Optional[StrBytes] OptionalInt = Optional[int] OptionalIntFloat = Union[OptionalInt, float] OptionalIntFloatDecimal = Union[OptionalIntFloat, Decimal] OptionalDate = Optional[date] StrIntFloat = Union[str, int, float] if TYPE_CHECKING: from typing_extensions import Annotated from .dataclasses import Dataclass from .main import BaseModel from .typing import CallableGenerator ModelOrDc = Type[Union[BaseModel, Dataclass]] T = TypeVar('T') _DEFINED_TYPES: 'WeakSet[type]' = WeakSet() @overload def _registered(typ: Type[T]) -> Type[T]: pass @overload def _registered(typ: 'ConstrainedNumberMeta') -> 'ConstrainedNumberMeta': pass def _registered(typ: Union[Type[T], 'ConstrainedNumberMeta']) -> Union[Type[T], 'ConstrainedNumberMeta']: # In order to generate valid examples of constrained types, Hypothesis needs # to inspect the type object - so we keep a weakref to each contype object # until it can be registered. When (or if) our Hypothesis plugin is loaded, # it monkeypatches this function. # If Hypothesis is never used, the total effect is to keep a weak reference # which has minimal memory usage and doesn't even affect garbage collection. _DEFINED_TYPES.add(typ) return typ class ConstrainedNumberMeta(type): def __new__(cls, name: str, bases: Any, dct: Dict[str, Any]) -> 'ConstrainedInt': # type: ignore new_cls = cast('ConstrainedInt', type.__new__(cls, name, bases, dct)) if new_cls.gt is not None and new_cls.ge is not None: raise errors.ConfigError('bounds gt and ge cannot be specified at the same time') if new_cls.lt is not None and new_cls.le is not None: raise errors.ConfigError('bounds lt and le cannot be specified at the same time') return _registered(new_cls) # type: ignore # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ BOOLEAN TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ if TYPE_CHECKING: StrictBool = bool else: class StrictBool(int): """ StrictBool to allow for bools which are not type-coerced. """ @classmethod def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: field_schema.update(type='boolean') @classmethod def __get_validators__(cls) -> 'CallableGenerator': yield cls.validate @classmethod def validate(cls, value: Any) -> bool: """ Ensure that we only allow bools. """ if isinstance(value, bool): return value raise errors.StrictBoolError() # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ INTEGER TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ class ConstrainedInt(int, metaclass=ConstrainedNumberMeta): strict: bool = False gt: OptionalInt = None ge: OptionalInt = None lt: OptionalInt = None le: OptionalInt = None multiple_of: OptionalInt = None @classmethod def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: update_not_none( field_schema, exclusiveMinimum=cls.gt, exclusiveMaximum=cls.lt, minimum=cls.ge, maximum=cls.le, multipleOf=cls.multiple_of, ) @classmethod def __get_validators__(cls) -> 'CallableGenerator': yield strict_int_validator if cls.strict else int_validator yield number_size_validator yield number_multiple_validator def conint( *, strict: bool = False, gt: Optional[int] = None, ge: Optional[int] = None, lt: Optional[int] = None, le: Optional[int] = None, multiple_of: Optional[int] = None, ) -> Type[int]: # use kwargs then define conf in a dict to aid with IDE type hinting namespace = dict(strict=strict, gt=gt, ge=ge, lt=lt, le=le, multiple_of=multiple_of) return type('ConstrainedIntValue', (ConstrainedInt,), namespace) if TYPE_CHECKING: PositiveInt = int NegativeInt = int NonPositiveInt = int NonNegativeInt = int StrictInt = int else: class PositiveInt(ConstrainedInt): gt = 0 class NegativeInt(ConstrainedInt): lt = 0 class NonPositiveInt(ConstrainedInt): le = 0 class NonNegativeInt(ConstrainedInt): ge = 0 class StrictInt(ConstrainedInt): strict = True # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ FLOAT TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ class ConstrainedFloat(float, metaclass=ConstrainedNumberMeta): strict: bool = False gt: OptionalIntFloat = None ge: OptionalIntFloat = None lt: OptionalIntFloat = None le: OptionalIntFloat = None multiple_of: OptionalIntFloat = None allow_inf_nan: Optional[bool] = None @classmethod def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: update_not_none( field_schema, exclusiveMinimum=cls.gt, exclusiveMaximum=cls.lt, minimum=cls.ge, maximum=cls.le, multipleOf=cls.multiple_of, ) # Modify constraints to account for differences between IEEE floats and JSON if field_schema.get('exclusiveMinimum') == -math.inf: del field_schema['exclusiveMinimum'] if field_schema.get('minimum') == -math.inf: del field_schema['minimum'] if field_schema.get('exclusiveMaximum') == math.inf: del field_schema['exclusiveMaximum'] if field_schema.get('maximum') == math.inf: del field_schema['maximum'] @classmethod def __get_validators__(cls) -> 'CallableGenerator': yield strict_float_validator if cls.strict else float_validator yield number_size_validator yield number_multiple_validator yield float_finite_validator def confloat( *, strict: bool = False, gt: float = None, ge: float = None, lt: float = None, le: float = None, multiple_of: float = None, allow_inf_nan: Optional[bool] = None, ) -> Type[float]: # use kwargs then define conf in a dict to aid with IDE type hinting namespace = dict(strict=strict, gt=gt, ge=ge, lt=lt, le=le, multiple_of=multiple_of, allow_inf_nan=allow_inf_nan) return type('ConstrainedFloatValue', (ConstrainedFloat,), namespace) if TYPE_CHECKING: PositiveFloat = float NegativeFloat = float NonPositiveFloat = float NonNegativeFloat = float StrictFloat = float FiniteFloat = float else: class PositiveFloat(ConstrainedFloat): gt = 0 class NegativeFloat(ConstrainedFloat): lt = 0 class NonPositiveFloat(ConstrainedFloat): le = 0 class NonNegativeFloat(ConstrainedFloat): ge = 0 class StrictFloat(ConstrainedFloat): strict = True class FiniteFloat(ConstrainedFloat): allow_inf_nan = False # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ BYTES TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ class ConstrainedBytes(bytes): strip_whitespace = False to_upper = False to_lower = False min_length: OptionalInt = None max_length: OptionalInt = None strict: bool = False @classmethod def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: update_not_none(field_schema, minLength=cls.min_length, maxLength=cls.max_length) @classmethod def __get_validators__(cls) -> 'CallableGenerator': yield strict_bytes_validator if cls.strict else bytes_validator yield constr_strip_whitespace yield constr_upper yield constr_lower yield constr_length_validator def conbytes( *, strip_whitespace: bool = False, to_upper: bool = False, to_lower: bool = False, min_length: Optional[int] = None, max_length: Optional[int] = None, strict: bool = False, ) -> Type[bytes]: # use kwargs then define conf in a dict to aid with IDE type hinting namespace = dict( strip_whitespace=strip_whitespace, to_upper=to_upper, to_lower=to_lower, min_length=min_length, max_length=max_length, strict=strict, ) return _registered(type('ConstrainedBytesValue', (ConstrainedBytes,), namespace)) if TYPE_CHECKING: StrictBytes = bytes else: class StrictBytes(ConstrainedBytes): strict = True # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ STRING TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ class ConstrainedStr(str): strip_whitespace = False to_upper = False to_lower = False min_length: OptionalInt = None max_length: OptionalInt = None curtail_length: OptionalInt = None regex: Optional[Union[str, Pattern[str]]] = None strict = False @classmethod def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: update_not_none( field_schema, minLength=cls.min_length, maxLength=cls.max_length, pattern=cls.regex and cls._get_pattern(cls.regex), ) @classmethod def __get_validators__(cls) -> 'CallableGenerator': yield strict_str_validator if cls.strict else str_validator yield constr_strip_whitespace yield constr_upper yield constr_lower yield constr_length_validator yield cls.validate @classmethod def validate(cls, value: Union[str]) -> Union[str]: if cls.curtail_length and len(value) > cls.curtail_length: value = value[: cls.curtail_length] if cls.regex: if not re.match(cls.regex, value): raise errors.StrRegexError(pattern=cls._get_pattern(cls.regex)) return value @staticmethod def _get_pattern(regex: Union[str, Pattern[str]]) -> str: return regex if isinstance(regex, str) else regex.pattern def constr( *, strip_whitespace: bool = False, to_upper: bool = False, to_lower: bool = False, strict: bool = False, min_length: Optional[int] = None, max_length: Optional[int] = None, curtail_length: Optional[int] = None, regex: Optional[str] = None, ) -> Type[str]: # use kwargs then define conf in a dict to aid with IDE type hinting namespace = dict( strip_whitespace=strip_whitespace, to_upper=to_upper, to_lower=to_lower, strict=strict, min_length=min_length, max_length=max_length, curtail_length=curtail_length, regex=regex and re.compile(regex), ) return _registered(type('ConstrainedStrValue', (ConstrainedStr,), namespace)) if TYPE_CHECKING: StrictStr = str else: class StrictStr(ConstrainedStr): strict = True # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ SET TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # This types superclass should be Set[T], but cython chokes on that... class ConstrainedSet(set): # type: ignore # Needed for pydantic to detect that this is a set __origin__ = set __args__: Set[Type[T]] # type: ignore min_items: Optional[int] = None max_items: Optional[int] = None item_type: Type[T] # type: ignore @classmethod def __get_validators__(cls) -> 'CallableGenerator': yield cls.set_length_validator @classmethod def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: update_not_none(field_schema, minItems=cls.min_items, maxItems=cls.max_items) @classmethod def set_length_validator(cls, v: 'Optional[Set[T]]') -> 'Optional[Set[T]]': if v is None: return None v = set_validator(v) v_len = len(v) if cls.min_items is not None and v_len < cls.min_items: raise errors.SetMinLengthError(limit_value=cls.min_items) if cls.max_items is not None and v_len > cls.max_items: raise errors.SetMaxLengthError(limit_value=cls.max_items) return v def conset(item_type: Type[T], *, min_items: Optional[int] = None, max_items: Optional[int] = None) -> Type[Set[T]]: # __args__ is needed to conform to typing generics api namespace = {'min_items': min_items, 'max_items': max_items, 'item_type': item_type, '__args__': [item_type]} # We use new_class to be able to deal with Generic types return new_class('ConstrainedSetValue', (ConstrainedSet,), {}, lambda ns: ns.update(namespace)) # This types superclass should be FrozenSet[T], but cython chokes on that... class ConstrainedFrozenSet(frozenset): # type: ignore # Needed for pydantic to detect that this is a set __origin__ = frozenset __args__: FrozenSet[Type[T]] # type: ignore min_items: Optional[int] = None max_items: Optional[int] = None item_type: Type[T] # type: ignore @classmethod def __get_validators__(cls) -> 'CallableGenerator': yield cls.frozenset_length_validator @classmethod def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: update_not_none(field_schema, minItems=cls.min_items, maxItems=cls.max_items) @classmethod def frozenset_length_validator(cls, v: 'Optional[FrozenSet[T]]') -> 'Optional[FrozenSet[T]]': if v is None: return None v = frozenset_validator(v) v_len = len(v) if cls.min_items is not None and v_len < cls.min_items: raise errors.FrozenSetMinLengthError(limit_value=cls.min_items) if cls.max_items is not None and v_len > cls.max_items: raise errors.FrozenSetMaxLengthError(limit_value=cls.max_items) return v def confrozenset( item_type: Type[T], *, min_items: Optional[int] = None, max_items: Optional[int] = None ) -> Type[FrozenSet[T]]: # __args__ is needed to conform to typing generics api namespace = {'min_items': min_items, 'max_items': max_items, 'item_type': item_type, '__args__': [item_type]} # We use new_class to be able to deal with Generic types return new_class('ConstrainedFrozenSetValue', (ConstrainedFrozenSet,), {}, lambda ns: ns.update(namespace)) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ LIST TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # This types superclass should be List[T], but cython chokes on that... class ConstrainedList(list): # type: ignore # Needed for pydantic to detect that this is a list __origin__ = list __args__: Tuple[Type[T], ...] # type: ignore min_items: Optional[int] = None max_items: Optional[int] = None unique_items: Optional[bool] = None item_type: Type[T] # type: ignore @classmethod def __get_validators__(cls) -> 'CallableGenerator': yield cls.list_length_validator if cls.unique_items: yield cls.unique_items_validator @classmethod def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: update_not_none(field_schema, minItems=cls.min_items, maxItems=cls.max_items, uniqueItems=cls.unique_items) @classmethod def list_length_validator(cls, v: 'Optional[List[T]]') -> 'Optional[List[T]]': if v is None: return None v = list_validator(v) v_len = len(v) if cls.min_items is not None and v_len < cls.min_items: raise errors.ListMinLengthError(limit_value=cls.min_items) if cls.max_items is not None and v_len > cls.max_items: raise errors.ListMaxLengthError(limit_value=cls.max_items) return v @classmethod def unique_items_validator(cls, v: 'Optional[List[T]]') -> 'Optional[List[T]]': if v is None: return None for i, value in enumerate(v, start=1): if value in v[i:]: raise errors.ListUniqueItemsError() return v def conlist( item_type: Type[T], *, min_items: Optional[int] = None, max_items: Optional[int] = None, unique_items: bool = None ) -> Type[List[T]]: # __args__ is needed to conform to typing generics api namespace = dict( min_items=min_items, max_items=max_items, unique_items=unique_items, item_type=item_type, __args__=(item_type,) ) # We use new_class to be able to deal with Generic types return new_class('ConstrainedListValue', (ConstrainedList,), {}, lambda ns: ns.update(namespace)) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PYOBJECT TYPE ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ if TYPE_CHECKING: PyObject = Callable[..., Any] else: class PyObject: validate_always = True @classmethod def __get_validators__(cls) -> 'CallableGenerator': yield cls.validate @classmethod def validate(cls, value: Any) -> Any: if isinstance(value, Callable): return value try: value = str_validator(value) except errors.StrError: raise errors.PyObjectError(error_message='value is neither a valid import path not a valid callable') try: return import_string(value) except ImportError as e: raise errors.PyObjectError(error_message=str(e)) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ DECIMAL TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ class ConstrainedDecimal(Decimal, metaclass=ConstrainedNumberMeta): gt: OptionalIntFloatDecimal = None ge: OptionalIntFloatDecimal = None lt: OptionalIntFloatDecimal = None le: OptionalIntFloatDecimal = None max_digits: OptionalInt = None decimal_places: OptionalInt = None multiple_of: OptionalIntFloatDecimal = None @classmethod def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: update_not_none( field_schema, exclusiveMinimum=cls.gt, exclusiveMaximum=cls.lt, minimum=cls.ge, maximum=cls.le, multipleOf=cls.multiple_of, ) @classmethod def __get_validators__(cls) -> 'CallableGenerator': yield decimal_validator yield number_size_validator yield number_multiple_validator yield cls.validate @classmethod def validate(cls, value: Decimal) -> Decimal: try: normalized_value = value.normalize() except InvalidOperation: normalized_value = value digit_tuple, exponent = normalized_value.as_tuple()[1:] if exponent in {'F', 'n', 'N'}: raise errors.DecimalIsNotFiniteError() if exponent >= 0: # A positive exponent adds that many trailing zeros. digits = len(digit_tuple) + exponent decimals = 0 else: # If the absolute value of the negative exponent is larger than the # number of digits, then it's the same as the number of digits, # because it'll consume all of the digits in digit_tuple and then # add abs(exponent) - len(digit_tuple) leading zeros after the # decimal point. if abs(exponent) > len(digit_tuple): digits = decimals = abs(exponent) else: digits = len(digit_tuple) decimals = abs(exponent) whole_digits = digits - decimals if cls.max_digits is not None and digits > cls.max_digits: raise errors.DecimalMaxDigitsError(max_digits=cls.max_digits) if cls.decimal_places is not None and decimals > cls.decimal_places: raise errors.DecimalMaxPlacesError(decimal_places=cls.decimal_places) if cls.max_digits is not None and cls.decimal_places is not None: expected = cls.max_digits - cls.decimal_places if whole_digits > expected: raise errors.DecimalWholeDigitsError(whole_digits=expected) return value def condecimal( *, gt: Decimal = None, ge: Decimal = None, lt: Decimal = None, le: Decimal = None, max_digits: Optional[int] = None, decimal_places: Optional[int] = None, multiple_of: Decimal = None, ) -> Type[Decimal]: # use kwargs then define conf in a dict to aid with IDE type hinting namespace = dict( gt=gt, ge=ge, lt=lt, le=le, max_digits=max_digits, decimal_places=decimal_places, multiple_of=multiple_of ) return type('ConstrainedDecimalValue', (ConstrainedDecimal,), namespace) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ UUID TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ if TYPE_CHECKING: UUID1 = UUID UUID3 = UUID UUID4 = UUID UUID5 = UUID else: class UUID1(UUID): _required_version = 1 @classmethod def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: field_schema.update(type='string', format=f'uuid{cls._required_version}') class UUID3(UUID1): _required_version = 3 class UUID4(UUID1): _required_version = 4 class UUID5(UUID1): _required_version = 5 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PATH TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ if TYPE_CHECKING: FilePath = Path DirectoryPath = Path else: class FilePath(Path): @classmethod def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: field_schema.update(format='file-path') @classmethod def __get_validators__(cls) -> 'CallableGenerator': yield path_validator yield path_exists_validator yield cls.validate @classmethod def validate(cls, value: Path) -> Path: if not value.is_file(): raise errors.PathNotAFileError(path=value) return value class DirectoryPath(Path): @classmethod def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: field_schema.update(format='directory-path') @classmethod def __get_validators__(cls) -> 'CallableGenerator': yield path_validator yield path_exists_validator yield cls.validate @classmethod def validate(cls, value: Path) -> Path: if not value.is_dir(): raise errors.PathNotADirectoryError(path=value) return value # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON TYPE ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ class JsonWrapper: pass class JsonMeta(type): def __getitem__(self, t: Type[Any]) -> Type[JsonWrapper]: if t is Any: return Json # allow Json[Any] to replecate plain Json return _registered(type('JsonWrapperValue', (JsonWrapper,), {'inner_type': t})) if TYPE_CHECKING: Json = Annotated[T, ...] # Json[list[str]] will be recognized by type checkers as list[str] else: class Json(metaclass=JsonMeta): @classmethod def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: field_schema.update(type='string', format='json-string') # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ SECRET TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ class SecretField(abc.ABC): """ Note: this should be implemented as a generic like `SecretField(ABC, Generic[T])`, the `__init__()` should be part of the abstract class and the `get_secret_value()` method should use the generic `T` type. However Cython doesn't support very well generics at the moment and the generated code fails to be imported (see https://github.com/cython/cython/issues/2753). """ def __eq__(self, other: Any) -> bool: return isinstance(other, self.__class__) and self.get_secret_value() == other.get_secret_value() def __str__(self) -> str: return '**********' if self.get_secret_value() else '' def __hash__(self) -> int: return hash(self.get_secret_value()) @abc.abstractmethod def get_secret_value(self) -> Any: # pragma: no cover ... class SecretStr(SecretField): min_length: OptionalInt = None max_length: OptionalInt = None @classmethod def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: update_not_none( field_schema, type='string', writeOnly=True, format='password', minLength=cls.min_length, maxLength=cls.max_length, ) @classmethod def __get_validators__(cls) -> 'CallableGenerator': yield cls.validate yield constr_length_validator @classmethod def validate(cls, value: Any) -> 'SecretStr': if isinstance(value, cls): return value value = str_validator(value) return cls(value) def __init__(self, value: str): self._secret_value = value def __repr__(self) -> str: return f"SecretStr('{self}')" def __len__(self) -> int: return len(self._secret_value) def display(self) -> str: warnings.warn('`secret_str.display()` is deprecated, use `str(secret_str)` instead', DeprecationWarning) return str(self) def get_secret_value(self) -> str: return self._secret_value class SecretBytes(SecretField): min_length: OptionalInt = None max_length: OptionalInt = None @classmethod def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: update_not_none( field_schema, type='string', writeOnly=True, format='password', minLength=cls.min_length, maxLength=cls.max_length, ) @classmethod def __get_validators__(cls) -> 'CallableGenerator': yield cls.validate yield constr_length_validator @classmethod def validate(cls, value: Any) -> 'SecretBytes': if isinstance(value, cls): return value value = bytes_validator(value) return cls(value) def __init__(self, value: bytes): self._secret_value = value def __repr__(self) -> str: return f"SecretBytes(b'{self}')" def __len__(self) -> int: return len(self._secret_value) def display(self) -> str: warnings.warn('`secret_bytes.display()` is deprecated, use `str(secret_bytes)` instead', DeprecationWarning) return str(self) def get_secret_value(self) -> bytes: return self._secret_value # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PAYMENT CARD TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ class PaymentCardBrand(str, Enum): # If you add another card type, please also add it to the # Hypothesis strategy in `pydantic._hypothesis_plugin`. amex = 'American Express' mastercard = 'Mastercard' visa = 'Visa' other = 'other' def __str__(self) -> str: return self.value class PaymentCardNumber(str): """ Based on: https://en.wikipedia.org/wiki/Payment_card_number """ strip_whitespace: ClassVar[bool] = True min_length: ClassVar[int] = 12 max_length: ClassVar[int] = 19 bin: str last4: str brand: PaymentCardBrand def __init__(self, card_number: str): self.bin = card_number[:6] self.last4 = card_number[-4:] self.brand = self._get_brand(card_number) @classmethod def __get_validators__(cls) -> 'CallableGenerator': yield str_validator yield constr_strip_whitespace yield constr_length_validator yield cls.validate_digits yield cls.validate_luhn_check_digit yield cls yield cls.validate_length_for_brand @property def masked(self) -> str: num_masked = len(self) - 10 # len(bin) + len(last4) == 10 return f'{self.bin}{"*" * num_masked}{self.last4}' @classmethod def validate_digits(cls, card_number: str) -> str: if not card_number.isdigit(): raise errors.NotDigitError return card_number @classmethod def validate_luhn_check_digit(cls, card_number: str) -> str: """ Based on: https://en.wikipedia.org/wiki/Luhn_algorithm """ sum_ = int(card_number[-1]) length = len(card_number) parity = length % 2 for i in range(length - 1): digit = int(card_number[i]) if i % 2 == parity: digit *= 2 if digit > 9: digit -= 9 sum_ += digit valid = sum_ % 10 == 0 if not valid: raise errors.LuhnValidationError return card_number @classmethod def validate_length_for_brand(cls, card_number: 'PaymentCardNumber') -> 'PaymentCardNumber': """ Validate length based on BIN for major brands: https://en.wikipedia.org/wiki/Payment_card_number#Issuer_identification_number_(IIN) """ required_length: Union[None, int, str] = None if card_number.brand in PaymentCardBrand.mastercard: required_length = 16 valid = len(card_number) == required_length elif card_number.brand == PaymentCardBrand.visa: required_length = '13, 16 or 19' valid = len(card_number) in {13, 16, 19} elif card_number.brand == PaymentCardBrand.amex: required_length = 15 valid = len(card_number) == required_length else: valid = True if not valid: raise errors.InvalidLengthForBrand(brand=card_number.brand, required_length=required_length) return card_number @staticmethod def _get_brand(card_number: str) -> PaymentCardBrand: if card_number[0] == '4': brand = PaymentCardBrand.visa elif 51 <= int(card_number[:2]) <= 55: brand = PaymentCardBrand.mastercard elif card_number[:2] in {'34', '37'}: brand = PaymentCardBrand.amex else: brand = PaymentCardBrand.other return brand # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ BYTE SIZE TYPE ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ BYTE_SIZES = { 'b': 1, 'kb': 10**3, 'mb': 10**6, 'gb': 10**9, 'tb': 10**12, 'pb': 10**15, 'eb': 10**18, 'kib': 2**10, 'mib': 2**20, 'gib': 2**30, 'tib': 2**40, 'pib': 2**50, 'eib': 2**60, } BYTE_SIZES.update({k.lower()[0]: v for k, v in BYTE_SIZES.items() if 'i' not in k}) byte_string_re = re.compile(r'^\s*(\d*\.?\d+)\s*(\w+)?', re.IGNORECASE) class ByteSize(int): @classmethod def __get_validators__(cls) -> 'CallableGenerator': yield cls.validate @classmethod def validate(cls, v: StrIntFloat) -> 'ByteSize': try: return cls(int(v)) except ValueError: pass str_match = byte_string_re.match(str(v)) if str_match is None: raise errors.InvalidByteSize() scalar, unit = str_match.groups() if unit is None: unit = 'b' try: unit_mult = BYTE_SIZES[unit.lower()] except KeyError: raise errors.InvalidByteSizeUnit(unit=unit) return cls(int(float(scalar) * unit_mult)) def human_readable(self, decimal: bool = False) -> str: if decimal: divisor = 1000 units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'] final_unit = 'EB' else: divisor = 1024 units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB'] final_unit = 'EiB' num = float(self) for unit in units: if abs(num) < divisor: return f'{num:0.1f}{unit}' num /= divisor return f'{num:0.1f}{final_unit}' def to(self, unit: str) -> float: try: unit_div = BYTE_SIZES[unit.lower()] except KeyError: raise errors.InvalidByteSizeUnit(unit=unit) return self / unit_div # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ DATE TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ if TYPE_CHECKING: PastDate = date FutureDate = date else: class PastDate(date): @classmethod def __get_validators__(cls) -> 'CallableGenerator': yield parse_date yield cls.validate @classmethod def validate(cls, value: date) -> date: if value >= date.today(): raise errors.DateNotInThePastError() return value class FutureDate(date): @classmethod def __get_validators__(cls) -> 'CallableGenerator': yield parse_date yield cls.validate @classmethod def validate(cls, value: date) -> date: if value <= date.today(): raise errors.DateNotInTheFutureError() return value class ConstrainedDate(date, metaclass=ConstrainedNumberMeta): gt: OptionalDate = None ge: OptionalDate = None lt: OptionalDate = None le: OptionalDate = None @classmethod def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: update_not_none(field_schema, exclusiveMinimum=cls.gt, exclusiveMaximum=cls.lt, minimum=cls.ge, maximum=cls.le) @classmethod def __get_validators__(cls) -> 'CallableGenerator': yield parse_date yield number_size_validator def condate( *, gt: date = None, ge: date = None, lt: date = None, le: date = None, ) -> Type[date]: # use kwargs then define conf in a dict to aid with IDE type hinting namespace = dict(gt=gt, ge=ge, lt=lt, le=le) return type('ConstrainedDateValue', (ConstrainedDate,), namespace)