restrainer

Lightweight, flexible validation library.


Keywords
validation, library, rest, api
License
Other
Install
pip install restrainer==1.4

Documentation

Restrainer

Allows you to easily validate your Python dictionaries. It provides easily parsable response, so I found it very fitting in REST APIs. You can also easily extend it to fit your needs, for example if you'd want to check uniqueness.

Instalation

It's compatible with Python 3.+

sudo pip install restrainer

Examples

Let's say we're handling user creation in REST API. Below is sample rules set.

{
    "username" : {
        "required" : True,
        "type" : "string",
        "between" : [3, 20]
    },
    "email" : {
        "required" : True,
        "type" : "string",
        "data_format" : "email"
    },
    "password" : {
        "required" : True,
        "type" : "string",
        "min" : 7
     }
}

Putting it in front of this object:

{
    "username" : "as",
    "email" : "sexsexsex"
}

will result with:

[
    {
        "field": "password",
        "constraint": "required"
    },
    {
        "min": 3,
        "max": 20,
        "field": "username",
        "constraint": "between"
    },
    {
        "field": "email",
        "data_format": "email",
        "constraint": "data_format"
    }
]

How to use it?

from restrainer import Validator

rules = {
    "username" : {
        "required" : True,
        "type" : "string",
        "between" : [3, 20]
    },
    "email" : {
        "required" : True,
        "type" : "string",
        "data_format" : "email"
    },
    "password" : {
        "required" : True,
        "type" : "string",
        "min" : 7
     }
}

data = {
    "username" : "as",
    "email" : "sexsexsex"
}

validator = Validator()
validator.validate(data, rules = rules) #This also returns errors list

if validator.fails():
    print(validator.errors())
else:
    print("VALIDATION SUCCESSFUL")

Nested lists/dicts

Sometimes you may want to have nested lists or dictionaries in your data set and validate them.

Nested dict

If you wanna have nested dict and validate it you can use 'properties' attribute:

rules = {
    "name" : {
        "type" : "string",
        "size" : 10, #Size in case of string means its length
        "required" : True
    },
    "pet" : {
        "required" : True,
        "type" : "object",
        "properties" : {
            "name" : {"type" : "string", "required" : True},
            "age" : {"min" : 0, "max" : 100}
        }
    }
}

data = {
    "name" : "Batman",
    "pet" : {
        "name" : "Rex",
        "age" : 200
    }
}

Validating it will reslut with:

[
    {
        "field": "name",
        "constraint": "size",
        "size": 10
    },
    {
        "pet": [
            {
                "field": "age",
                "constraint": "max",
                "max": 100
            }
        ]
    }
]

List of dicts

Simiral to nested dict, but this time you should use 'items' keyword.

rules = {
    "name" : {
        "type" : "string",
        "size" : 10, #Size in case of string means its length
        "required" : True
    },
    "pets" : {
        "required" : True,
        "type" : "list",
        "items" : {
            "name" : {"type" : "string", "required" : True},
            "age" : {"min" : 0, "max" : 100}
        }
    }
}

data =  {
    "name" : "Jerry",
    "pets" : [
        {"name" : "Dog 1", "age" : 101},
        {"name" : "Dog 2", "age" : 4},
        {"name" : "Dog 3", "age 4"},
        {"age" : 32}
    ]
}

Responds with:

[
    {
        "pets": [
            {
                "index": 0,
                "field": "age",
                "constraint": "max",
                "max": 100
            },
            {
                "index": 2,
                "field": "name",
                "constraint": "required"
            }
        ]
    },
    {
        "size": 10,
        "index": 2,
        "field": "name",
        "constraint": "size"
    }
]

List of primitives

This is need a little bit more work on creating new constraints. Currently you can only validate type of items in array of primitives(numeric, string, integer, boolean etc.). You do it like that:

"ids" : {
    "type" : "list",
    "list_type" : "integer"
} 

Built-in constrainers

Name Accepted Values
type string
list_type string
required boolean
value array ex. ["male", "female"]
min numeric
max numeric
between array of 2 numeric ex. [2, 10]
size integer
data_format string - email/mac/ip
regex string - regular expression
validator dictionary - {"function": f, "message" : "error msg"}
coerce callable

Validator constrainer

Allows you to pass function which shall return True/False and accept one argument. This argument will be data passed by user. And if you return False validation will fail.

Coerce constrainer

Coerce function should take one argument and return some value. Let's say you accept numeric and if you pass coerce function divide_by_two, final version in document will be changed to initial value divided by two.

Custom constrainers

Library lets you easily build your custom constrainers. You just need to create class inheriting from Constraint. It has to have two methods:

  • name(self) - returning string value of how you wanna invoke constraint in rules set;
  • validate(self, value, constraint_value, field, doc) - where you perform your logic and return True or False. IMPORTANT: Returning dict will result in validation fail. You should return dict when you want to pass additional info to error output. Like for "between" constraint you have additional "min", "max" fields in error output.
  • accept_null(self) - optional. It's False by default. If value in validated document is None, validation method won't be invoked. You would want to switch it to True in presence constraints or something like that.

Example

from restrainer import Constraint, Validator

#define constrainer
class DifferentThanConstraint(Constraint):

    #key to which you will define value for specific constrain
    def name(self):
        return "different"

    def validate(self, value, constraint_value, field, doc):
        if value == constraint_value: #if value in document is equal to value in rules set
            return {"different" : constraint_value}
        return True 

#create validator     
rules {
    "name" : {
        "type" : "string",
        "different" : "admin"
    }
 }
validator = Validator(rules = rules)
#load custom constrainer
validator.load_constraint(DifferentThanConstraint())

data = {
    "name" : "admin"
}

#run validator
validator.validate(data)
print(validator.errors())

Snippet above will print with:

{
    "field" : "name",
    "constraint" : "different",
    "different" : "admin"
}

This implementation of diffrentThan constrainer is preety poor, because it will only check if passed parrameters are the same. But you could rewrite it to accept list as constrainer_value and make sure if there are no same items.

Running tests

    python -m unittest discover tests