Skip to main content

Annotation based python object validator

Project description

Endorser

A lightweight data validation and converter package for Python 3.6+. It's always better to work with a structured set of data instead of just a simple dictionary. This package provides an easy way to do the conversion seamlessly while it provides a set of tools to validate the data. The main purpose of this package is to create structured data from unstructured types while validating it.

from validation.converter import DocumentConverter
from validation.schema import Schema
from validation.validator import min_size

class Address(Schema):
    zip_code: str
    house_number: int
    addition: str = None

class User(Schema):
    email: str
    username: str
    firstname: str = None  # assigning None as default makes it optional during instantiation
    address: Address = None  # nest Schema classes

    @min_size(5)
    def validate_username(self, username):
        """Validates the username field, it has to be at least 5 chars long"""
        return username

data = {
    "email": "example@email.com",
    "username": "krisz",
    "address": {
        "zip_code": "6757",
        "house_number": 12,
        "addition": "A1"
    }
}

converter = DocumentConverter()
user = converter.convert(data, User) # converts the data dictionary to a User object
assert type(user) is User
assert user.email is "example@email.com"
assert type(user.address) is Address
assert user.address.zip_code is "6757"

Features

validation.schema.Schema

Base class for documents.

  • Must not be instantiated directly
  • Every attribute must be type hinted
  • As of now, supported type hints are the primivites, list, dict, typing.List and subclasses of Schema
  • Every subclass of Schema must be considered as final and immutable
class User(Schema):
    email: str
    username: str
    firstname: str = None  # assigning None as default makes it optional
    age: int = 0  # assigning anything will be used as default value
    address: Address = None  # must be an instance of Schema

Note that it's possible for every attribute to have None as it's value, the default None only means that the value can be omitted from the document. If you want to make sure that the value cannot be None, apply the @validator.not_none decorator:

class User(Schema):
    email: str
    username: str = None

user = User(email="some@email.com")  # valid, as username can be omitted
user = User(email=None)  # valid, as email can have the value None

    ...
    from validation.validator import not_none

    @not_none
    def valid_email(self, email):
        return email

user = User(email=None)  # not valid, as it's both mandatory and cannot be None

Validation

You can validate Schema objects with following this convention:

from validation.validator import min_size

class SomeDocument(Schema):
    some_prop: str

    @min_size(5)  # has to be at least 5 chars long
    def validate_some_prop(self, value):
        return value

Every validation method has to start with the validate_ prefix followed by the name of the property. The value argument is the value which will be set during instantiation. The method has to return the value as we set this value on the object. You can see all validation methods in the validation.validator package.

Custom validation

You can either create a new decorator and apply it on the validator (for examples see the validation.validator package) or apply the validation on the validation method itself.

from validation import construct_error

class SomeDocument(Schema):
    some_prop: str

    @some_custom_validator  # apply custom decorators
    def validate_some_prop(self, value):
        for c in value:
            if c is " ":
                self.instance_errors.append(construct_error(
                    "some_prop", "cannot contain spaces"))
        return value  # make sure to always return the value

Alter values

It's possible to alter the value of Schema objects during validation:

import uuid
from validation.validator import valid_uuid

class User(Schema):
    id: uuid.UUID
    email: str
    username: str = None

    @valid_uuid  # ensures that the ID is a valid UUID
    def validate_id(self, id): 
        return uuid.UUID(id)

user = User(id="7b4f95e3-4fbe-4f94-838f-c34950240274", 
            email="some@email.com")
assert isinstance(user.id, uuid.UUID)

You can also create custom decorators to modify property values. Note that we hinted the id property to be of type uuid.UUID but we instantiate it with a string value. You are responsible to return the correct value type which you defined on the Schema class.

Instantiation

You have to use keyword arguments to instantiate a Schema object:

user = User(email="some@email.com", username="krisz")

You can set the _allow_unknown property on any Schema object to allow unknown properties:

user = User(_allow_unknown=True, email="some@email.com", unknown_prop="any value")
assert user.unknown_prop == "any value"

Validation happens during the instantiation of the Schema object. Note that there aren't any exception raised, you have to check if there were any errors yourself:

import uuid
from validation.validator import valid_uuid

class User(Schema):
    id: uuid.UUID
    email: str
    username: str = None

    @valid_uuid  # ensures that the ID is a valid UUID
    def validate_id(self, id): 
        return uuid.UUID(id)

user = User(id="invalid-uuid", 
            email="some@email.com")
assert user.id == "invalid-uuid"
if user.instance_errors:
    for error in user.instance_errors:
        print("invalid value for property %s: %s" % (error["field"], error["error"]))

You can use the obj.instance_errors property to check for errors on the instance and obj.doc_errors to check for validation errors on the whole document. This means if you have nested Schema objects, this property will return every error on every object from the root object:

from validation.schema import Schema
from validation.validator import min_size

class Address(Schema):
    zip_code: str
    house_number: int
    addition: str = None

class User(Schema):
    email: str
    username: str
    firstname: str = None 
    address: Address = None

    @min_size(5)
    def validate_username(self, username):
        return username

user = User(email="some@email.com", username="Joe", 
            address=Address(zip_code="67ZZ", 
            house_number="invalid_type"))  # validation fails on username and house_number
assert len(user.instance_errors) == 1
assert len(user.address.instance_errors) == 1
assert len(user.doc_errors) == 2

DocumentConverter

The DocumentConverter class is used to build structured data from a document. A document can either be a dictionary or a list of dictionaries. The DocumentConverter uses the Schema class to validate and build the objects from the document.

from validation.converter import DocumentConverter
from validation.schema import Schema
from validation.validator import min_size

class Address(Schema):
    zip_code: str
    house_number: int
    addition: str = None

class User(Schema):
    email: str
    username: str
    firstname: str = None
    address: Address = None

    @min_size(5)
    def validate_username(self, username):
        """Validates the username field, it has to be at least 5 chars long"""
        return username

data = {
    "email": "example@email.com",
    "username": "krisz",
    "address": {
        "zip_code": "6757",
        "house_number": 12,
        "addition": "A1"
    }
}

converter = DocumentConverter()
user = converter.convert(data, User)
assert type(user) is User
assert user.email is "example@email.com"
assert type(user.address) is Address
assert user.address.zip_code is "6757"

The DocumentConverter#convert method raises a ConversionError if validation fails. It holds the error messages in the ConversionError.errors list. You can pass the allow_unknown=True property to the convert method to allow unknown properties:

class SomeClass(Schema):
    prop: str
data = {
    "prop": "some property",
    "unknown_prop": "not defined on the class"
}

converter = DocumentConverter()
some_obj = converter.convert(data, SomeClass, allow_unknown=True)
assert some_obj.unknown_prop == "not defined on the class"

You can also pass a list of objects to the converter:

data = [{
        "prop": "a property"
    }, {
        "prop": "another property"
}]

list_of_objs = converter.convert(data, List[SomeClass])
assert type(list_of_objs) is list
assert type(list_of_objs[0]) is SomeClass
assert len(list_of_objs) == 2

Examples

For more examples see the test.example package.

Limitations

  • Only works on Python 3.6+
  • Currently supported types are all primitives, list, dict and typing.List, and of course other Schema objects as well
  • Classes which inherit from Schema are effectively final, you must not inherit from them

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

endorser-0.13.tar.gz (12.4 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

endorser-0.13-py3.6.egg (27.6 kB view details)

Uploaded Egg

endorser-0.13-py3-none-any.whl (13.0 kB view details)

Uploaded Python 3

File details

Details for the file endorser-0.13.tar.gz.

File metadata

  • Download URL: endorser-0.13.tar.gz
  • Upload date:
  • Size: 12.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No

File hashes

Hashes for endorser-0.13.tar.gz
Algorithm Hash digest
SHA256 54b54562f90f0c7d1a837be1272419e8bb378326889e5a31df25d95bc0113cb6
MD5 8a8c012367f37ee2ac7651c9a373a9b5
BLAKE2b-256 93bc80ad77a643c3a0848e3e15218ae5f1b31a5d4cc38a3c54fb6ccbfa670099

See more details on using hashes here.

File details

Details for the file endorser-0.13-py3.6.egg.

File metadata

  • Download URL: endorser-0.13-py3.6.egg
  • Upload date:
  • Size: 27.6 kB
  • Tags: Egg
  • Uploaded using Trusted Publishing? No

File hashes

Hashes for endorser-0.13-py3.6.egg
Algorithm Hash digest
SHA256 23358bc7c79731c8a2c5a33bccf57fc5a7a960671eac1ad89c6fde1dd33b9fbf
MD5 f4a591747aa82a7a2e542d886feff7ab
BLAKE2b-256 f5ece87a1eaaace2a747e719a51831164448c5d5604bcf5a583bd38af3a8ad99

See more details on using hashes here.

File details

Details for the file endorser-0.13-py3-none-any.whl.

File metadata

File hashes

Hashes for endorser-0.13-py3-none-any.whl
Algorithm Hash digest
SHA256 15ba2eb803ab2b8983a65921cb871d4c43704e9396b3ae98d1d6ed40eb21ec3d
MD5 3ddf3dfa29c9f9bef73d594eb7978beb
BLAKE2b-256 151372889743f392a3774de0077ffaad36af9b352a622956ff7d3e64aa1bf954

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page