Source code for cion.validators

"""Validators for use in a :class:`cion.Field`"""
import re
from typing import Any, Callable, Iterable, Literal, Optional
from uuid import UUID

from cion.exceptions import ValidatorError

__all__ = (
    "length",
    "range_",
    "one_of",
    "not_one_of",
    "equal_to",
    "uuid",
    "regex",
    "url",
    "email",
)

InnerValidator = Callable[[Any], Any]


[docs]def length( minimum: Optional[int] = None, maximum: Optional[int] = None, *, equal_to: Optional[int] = None, base_error_message: str = "Length must be ", error_messages: Optional[dict[str, str]] = None, ) -> InnerValidator: """Validates the length of a value This can be a string, list, or something like a dictionary, or basically any iterable (anything that len(value) can be called on) Args: minimum (int): The minimum length that the value can be maximum (int): The maximum length that the value can be equal_to (int): A number that the length of the value must equal. Cannot be used with minimum and maximum base_error_message (str): The start of the error message. View source for exact example error_messages (dict): A dictionary of the error messages that are allowed to be raised. If ``equal_to`` is specified, then the ``equal_to`` key is used and added on after ``base_error_message`` If ``minimum`` or ``maximum`` is specified, then the respective key is used and added on after ``base_error_message`` If both ``minimum`` and ``maximum`` are specified, then minimum and maximum error message is added together with ``and`` and then that is added on after ``base_error_message`` Note: The error message implementation is a bit complicated for this validator, it is recommended that you look at the source code for this function for more information Returns: InnerValidator: The inner function that is called when validating schema """ error_message = base_error_message _error_messages = { "equal_to": "equal to {equal_to}", "minimum": "greater than {minimum}", "maximum": "less than {maximum}", } _error_messages.update(error_messages or {}) if equal_to is not None: error_message += _error_messages["equal_to"].format(equal_to=equal_to) else: if minimum is not None and maximum is not None: error_message += _error_messages.get( "both", (f"{_error_messages['minimum']} and {_error_messages['maximum']}").format( minimum=minimum, maximum=maximum, equal_to=equal_to ), ) elif minimum is not None: error_message += _error_messages["minimum"].format(minimum=minimum) elif maximum is not None: error_message += _error_messages["maximum"].format(maximum=maximum) def inner(value: str): if equal_to is None: if minimum is not None and len(value) < minimum: raise ValidatorError(error_message) if maximum is not None and len(value) > maximum: raise ValidatorError(error_message) elif len(value) != equal_to: raise ValidatorError(error_message) return value return inner
[docs]def range_( minimum: Optional[int] = None, maximum: Optional[int] = None, error_message: str = "Number must be between {minimum} and {maximum}", ) -> InnerValidator: """Validates that a number is in a range Args: minimum (int): The minimum length that the number can be maximum (int): The maximum length that the number can be error_message (str): The error message that is raised You can use the ``minimum`` and ``maximum`` variables in the error message """ def inner(value: int): if minimum is not None and value < minimum: raise ValidatorError(error_message.format(minimum=minimum, maximum=maximum or "infinity")) if maximum is not None and value > maximum: raise ValidatorError(error_message.format(minimum=minimum or "0", maximum=maximum)) return value return inner
[docs]def one_of(*values: Any, error_message: str = "Value must be one of {values}") -> InnerValidator: """Checks if the value is in a list of values Used as a constraint on a field with :class:`cion.Schema` No assumptions are made about the type of the value Args: values: Any non-keyword arguments are valid values error_message (str): The error message that is raised The `values` variable can be used, and it is the values joined together by a string Returns: InnerValidator: The inner function that is called when validating schema """ def inner(value: Any): if value not in values: raise ValidatorError(error_message.format(values=", ".join(str(v) for v in values))) return value return inner
[docs]def not_one_of(*values: Iterable[Any], error_message: str = "Value must not be one of {values}") -> InnerValidator: """Checks if the value is not in a list of values Used as a constraint on a field with :class:`cion.Schema` No assumptions are made about the type of the value Args: values: Any non-keyword arguments are valid values error_message (str): The error message that is raised The `values` variable can be used, and it is the values joined together by a string Returns: InnerValidator: The inner function that is called when validating schema """ def inner(value: Any): if value in values: raise ValidatorError(error_message.format(values=", ".join(str(v) for v in values))) return value return inner
[docs]def equal_to(value_: Any, error_message: str = "Must be equal to {equal_to}") -> InnerValidator: """Validator that ensures a value is equal to a certain value No assumptions are made about types of any kind Args: value_: The item that the value must be equal to error_message: The error message that is raised when the value does not validate properly. You can use the ``equal_to`` variable in the message """ def inner(value: Any) -> Any: if value != value_: raise ValidatorError(error_message.format(equal_to=str(equal_to))) return value return inner
def uuid(error_message: str = "Must be a valid UUID", **kwargs) -> InnerValidator: def inner(value: str) -> UUID: try: transformed = UUID(value, **kwargs) except (ValueError, AttributeError, TypeError): raise ValidatorError(error_message) return transformed return inner
[docs]def regex( pattern: str, *, cast: bool = False, function: Literal["match", "fullmatch", "search"] = "fullmatch", flags: Any = 0, error_message="Must match regex", **kwargs: Any, ) -> InnerValidator: """Match a value against a regex pattern Note: It is highly recommended that you customize the error message, as regex functions tend to be application specific, and not generic. Args: pattern: The regex pattern that you want to match a string against cast: The value is always casted to a string, but if you wish to preserve the initial value, set this to ``False`` function (One of ``match``, ``fullmatch``, ``search``): What regex function to use against the value. View the regex docs for information about these https://docs.python.org/3/library/re.html#re.fullmatch TL,DR: Match matches at the start, fullmatch matches the entire string, and search searches the string for something to match flags: Flags to pass to :meth:``re.compile``. https://docs.python.org/3/library/re.html#re.A error_message: The error message raised when the match object returned when matching is None. kwargs: Arguments to pass to the method used for matching (again, see regex docs related to the function) """ prog = re.compile(pattern, flags=flags) to_call = getattr(prog, function) def inner(value: str) -> Any: casted = str(value) match = to_call(casted, **kwargs) if match is None: raise ValidatorError(error_message) return casted if cast is True else value return inner
[docs]def url(schemes: list[str] = ["http", "https"], *, error_message: str = "Must be a valid URL") -> InnerValidator: """Validates that a value is a valid URL This implementation uses :func:`regex` under the hood Args: schemes: A list of http schemes that will be valid error_message: The error message to raise if a value is not a valid URL """ return regex( rf"({'|'.join(schemes)}):\/\/[-a-zA-Z0-9@:%_\+~#=]{{1,256}}\.[a-z]{{1,25}}[-a-zA-Z0-9@:%_\+.~#?&//=]", cast=True, flags=re.IGNORECASE, error_message=error_message, )
[docs]def email(require_tld: bool = True, *, error_message="Must be an email") -> InnerValidator: """Validates an email address There are very many ways to match email addresses, ranging from just requiring the ``@`` symbol, to a large and slow regex. This email validator is under development, and will, for the most part, validate valid email addresses: ie, no valid email address will be rejected. However, there are a great many strings that will still match, valid email or not. If you have your own method if validating email addresses, you can do that with a custom validator. Args: require_tld: Whether or not to require a TLD, like, ".com". Example: with ``require_tld=False``, ``meizuflux@example`` will be valid. With ``require_tld=True``, it will not be, but adding a ``.com`` will make it valid. error_message: The error message to raise if the value is not a valid email """ to_compile = r"^(?=.{6,254}$)[0-9a-zA-Z_.+-]{1,249}@[0-9a-zA-Z_.-]{1,249}" + ( r"\..{2,24}$" if require_tld else r"$" ) return regex(to_compile, cast=False, flags=re.IGNORECASE, error_message=error_message)