Table Of Contents

Introduction

Portafilter provides several different approaches to validate your application's incoming data. It is most common to use a "Validator" with the built-in rules. However, we will discuss other approaches to validation as well.
Portafilter includes a wide variety of convenient validation rules that you may apply to data. We'll cover each of these validation rules in detail so that you are familiar with all of Portafilter's validation features.

Installation

Portafilter is zero dependencies and easy to install. It's on PyPI and all you need to do is:
pip install portafilter

Quickstart

To learn about Portafilter's powerful validation features, let's look at the basic usage of validating data and getting the error messages. By reading this overview, you'll be able to gain a good general understanding of how to validate incoming data using Portafilter:

Basic Usage

First, import and create the validator. It takes two main parameters, the input data and the validation rules:
from portafilter import Validator

validator = Validator(
    {
        'name': 'espresso',
        'description': 'Espresso is both a coffee beverage and a brewing method.',
    },
    {
        'name': 'required',
        'description': 'required|max:255',
    }
)
Next, check the validation state and get the error messages:
if validator.fails():
    # The data is not valid
    print(validator.errors())

# The data is valid
Or use the "validate" method directly:
from portafilter.exceptions import ValidationError

try:
    validator.validate()

    # The data is valid

except ValidationError as e:
    # The data is not valid
    print(validator.errors())

Validation Error Messages Format

Below, you can review an example of the validation errors:
from portafilter import Validator

validator = Validator(
    {
        'name': 10,
        'description': 'Espresso is both a coffee beverage and a brewing method.',
        'ingredients': [
            {
                'name': 'Robusta',
            },
            {
                'price': 2,
            },
            'Microfoam',
        ],
    },
    {
        'name': 'required|string|min:5',
        'description': 'required|max:255',
        'ingredients.*': 'required|dict',
        'ingredients.*.name': 'required|string',
    }
)

if validator.fails():
    print(validator.errors())
And, the error messages are:
{
    "ingredients.1.name":
    [
        "The ingredients.1.name field is required.",
        "The ingredients.1.name must be a string."
    ],
    "ingredients.2":
    [
        "The ingredients.2 must be a dictionary."
    ],
    "ingredients.2.name":
    [
        "The ingredients.2.name field is required.",
        "The ingredients.2.name must be a string."
    ],
    "name":
    [
        "The name must be a string.",
        "The name must be at least 5 characters."
    ]
}
Note that nested error keys are flattened into "dot" notation format.

Available Validation Rules

required

The field under validation must be present in the input data and not empty. A field is considered "empty" if one of the following conditions are true:
  • The value is null.
  • The value is an empty string.
  • The value is an empty list or empty dictionary.

nullable

The field under validation may be null.

string

The field under validation must be a string. If you would like to allow the field to also be null, you should assign the nullable rule to the field.

integer

The field under validation must be an integer.

numeric

The field under validation must be numeric.

list

The field under validation must be a list. Also, You can specify the list item value's type as the additional parameter:
from portafilter import Validator

validator = Validator(
    {
        'menu': ['Espresso', 'Mocha', 'Latte'],
    },
    {
        'coffee': 'list', # 'coffee': 'list:string',
    }
)

print(validator.fails()) # It returns False
The acceptable types are: dict, string, integer, numeric, boolean.

dict

The field under validation must be a dictionary. When additional values are provided to the dict rule, each key in the input dictionary must be present within the list of values provided to the rule. In the following example, the "available" key in the input dictionary is invalid since it is not contained in the list of values provided to the list rule:
from portafilter import Validator

validator = Validator(
    {
        'coffee': {
            'name': 'Frappe',
            'description': 'A Frappe coffee is a Greek iced coffee drink made from instant coffee.',
            'available': True,
        },
    },
    {
        'coffee': 'dict:name,description',
    }
)

if validator.fails():
    print(validator.errors())

boolean

The field under validation must be boolean.

email

The field under validation must be formatted as an email address.

size

The field under validation must have a size matching the given value. For string data, value corresponds to the number of characters. For numeric data, value corresponds to a given integer value (the attribute must also have the numeric or integer rule). For an list, size corresponds to the count of the list. For files, size corresponds to the file size in kilobytes. Let's look at some examples:
// Validate that a string is exactly 12 characters long
'title': 'size:12'

// Validate that a provided integer equals 10
'count': 'integer|size:10'

// Validate that a provided numeric equals 10
'weight': 'numeric|size:10.2'

// Validate that an list has exactly 5 elements
'menu': 'list|size:5'

min:value

The field under validation must have a minimum value. Strings, numerics, and lists are evaluated in the same fashion as the size rule.

max:value

The field under validation must be less than or equal to a maximum value. Strings, numerics, and lists are evaluated in the same fashion as the size rule.

between:min,max

The field under validation must have a size between the given min and max (inclusive). Strings, numerics, and lists are evaluated in the same fashion as the size rule.
Also, you can apply the between rule on dates:
from portafilter import Validator

validator = Validator(
    {
        'date': '2023-01-06',
    },
    {
        'date': 'date|between:2023-01-04,2023-01-10',
    }
)

print(validator.fails()) # It returns False

in:foo,bar,...

The field under validation must be included in the given list of values. By default, It checks the given values as strings but for the numeric values, It will cast the given list values to float and checks the rule:
from portafilter import Validator

validator = Validator(
    {
        'weight': 10.2,
    },
    {
        'weight': 'numeric|in:0,10,10.1,10.5,11',
    }
)

print(validator.fails()) # It returns False
However, you can use the "InRule" class directly to include any specific type:
from portafilter import Validator
from portafilter.rules import InRule

validator = Validator(
    {
        'date': 10.1,
    },
    {
        'date': ['numeric', InRule(0, 10, 10.1, 10.5, 11)],
    }
)

print(validator.fails())  # It returns False

not_in:foo,bar,...

The field under validation must not be included in the given list of values. The conditions are the same as the "In" rule.

contains:foo,bar,...

The field under validation must contain all the specified values. It will apply to the following types: string, list, and dict:
from portafilter import Validator

validator = Validator(
    {
        'ingredients': ['water', 'microfoam', 'robusta', 100],
    },
    {
        'ingredients': 'required|contains:water,microfoam,100',
    }
)

validator.fails() # It returns True
To detect the item types in lists, you can use the "ContainRule" directly:
from portafilter import Validator
from portafilter.rules import ContainsRule

validator = Validator(
    {
        'ingredients': ['water', 'microfoam', 'robusta', 100],
    },
    {
        'ingredients': ['required', ContainsRule('water', 'microfoam', 100)],
    }
)

print(validator.fails()) # It returns False

contains_one_of:foo,bar,...

The field under validation must contain at least one of the specified values. The conditions are the same as the "contains" rule.

starts_with:foo,bar,...

The string field under validation must start with one of the given values.

ends_with:foo,bar,...

The string field under validation must end with one of the given values.

regex:pattern

The string field under validation must match the given regular expression.

same:field

The given field must match the field under validation.

different:field

The field under validation must have a different value than field.

date

The field under validation must be a valid, non-relative date according to the "datetime.strptime" function. Also, you can specify the date format as the additional parameter. (The default format is: %Y-%m-%d)
from portafilter import Validator

validator = Validator(
    {
        'date': '2023-01-06 12:00:00',
    },
    {
        'date': 'date:%Y-%m-%d %H:%M:%S',
    }
)

print(validator.fails()) # It returns False

before:date

The field under validation must be a value preceding the given date. The date must be in "%Y-%m-%d" format. Also, the following special values are accepted: today, yesterday, tomorrow:
from portafilter import Validator

validator = Validator(
    {
        'date': '2023-01-05',
    },
    {
        'date': 'required|date|before:today',
    }
)

print(validator.fails()) # It returns False
Instead of passing a date string, you may specify another field to compare against the date:
from portafilter import Validator

validator = Validator(
    {
        'start_date': '2023-01-05',
        'end_date': '2023-01-06',
    },
    {
        'start_date': 'required|before:end_date',
        'end_date': 'required|date',
    }
)

print(validator.fails()) # It returns False

after:date

The field under validation must be a value after a given date. The conditions are the same as the "before" rule.

before_or_equal:date

The field under validation must be a value preceding or equal to the given date. The conditions are the same as the "before" rule.

after_or_equal:date

The field under validation must be a value after or equal to the given date. The conditions are the same as the "before" rule.

Validating Nested Input

Validating nested data based form input fields doesn't have to be a pain. You may use "dot notation" to validate nested attributes:
from portafilter import Validator

validator = Validator(
    {
        'coffee': {
            'name': 'Doppio',
        },
    },
    {
        'coffee.name': 'required|string|in:Doppio,Espresso,Lungo',
    }
)

print(validator.fails()) # It returns False
You may also validate each element of a list:
from portafilter import Validator

validator = Validator(
    {
        'menu': [
            {
                'id': 1,
                'name': 'Espresso',
            },
            {
                'id': 2,
                'name': None,
            },
            {
                'name': 'Flat white',
            },
        ],
    },
    {
        'menu.*.id': 'required|integer',
        'menu.*.name': 'required|string',
    }
)

if validator.fails():
    print(validator.errors())
And the errors are:
{
    "menu.1.name":
    [
        "The menu.1.name field is required.",
        "The menu.1.name must be a string."
    ],
    "menu.2.id":
    [
        "The menu.2.id field is required.",
        "The menu.2.id must be an integer."
    ]
}

Custom Validation Rules

Portafilter provides a variety of helpful validation rules; however, you may wish to specify some of your own. To do that, you may define your own rules.

Creating Custom Rule

To create a "Custom Rule", you must create a class inherited from the Portafilter "Rule" class.
A rule class contains two main methods: "passes" and "message". The passes method receives the attribute value, name, and a list of additional parameters. It must return True or False depending on whether the attribute value is valid or not. The message method should return the validation error message that should be used when validation fails:
from portafilter import Rule

class AgeVerificationRule(Rule):

    def passes(self, attribute, value, params) -> bool:
        """Determine if the validation rule passes.

        Arguments:
            attribute {str}
            value {Any}
            params {List[str]}

        Returns:
            bool
        """
        age_check = self.get_params()[0] if self.get_params() else 18

        return isinstance(value, int) and value >= age_check

    def message(self, attribute, value, params) -> str:
        """The validation error message.

        Arguments:
            attribute {str}
            value {Any}
            params {List[str]}

        Returns:
            str
        """
        return f'The {attribute} must be greater than {params[0]}.'
Now, assign the custom rule to the attributes. You can create an instance of the rule with the proper additional parameters or just pass the reference of it:
from portafilter import Validator

validator = Validator(
    {
        'age': 18,
    },
    {
        'age': ['required', 'integer', AgeVerificationRule],
    }
)

print(validator.fails()) # It returns False
The additional parameters must pass as the arguments: CustomRule(foo, bar, ...)
'age': ['required', 'integer', AgeVerificationRule(18)]

Creating Ruleset

A "Ruleset" is a combination of multiple rules placed in one place. Even you can use a ruleset in another one. It's reusable and prevents the rules duplication:
from portafilter import Ruleset

class EmailRuleset(Ruleset):

    rules = 'string|email'


class CustomEmailRuleset(Ruleset):

    rules = [EmailRuleset, EmailDomainRule]
Now, you are able to assign a ruleset or multiple rulesets to an attribute:
from portafilter import Validator

validator = Validator(
    {
        'admin_email': 'espresso@portafilter.dev',
        'customer_email': 'aryan.arabshahi.programmer@gmail.com',
    },
    {
        'admin_email': ['required', EmailRuleset, CustomEmailRuleset],
        'customer_email': EmailRuleset,
    }
)

Validation Decorator

The "validate" decorator allows the arguments passed to a function to be validated using the "Portafilter validation rules" before the function is called. While under the hood this uses the regular validator; it provides an extremely easy way to apply validation to your code with minimal boilerplate.
from portafilter import validate
from portafilter.exceptions import ValidationError


class EspressoMachine:

    @validate(name='required|string', microfoam='required|boolean', chocolate='required|numeric', sugar='boolean')
    def make_coffee(self, name, microfoam, chocolate, sugar = False) -> None:
        pass

try:
    EspressoMachine().make_coffee('Espresso', True, chocolate=10, sugar=None)

except ValidationError as e:
    print(e.get_errors())