tiramisu/doc/validator.md

14 KiB

Validator

What are validator ?

Validator is a functionnality that allow you to call a function to determine if an option is valid.

To define validator we have to use calculation object. The function have to raise a "ValueError" object if the value is not valid. It could emit a warning when raises a "ValueWarning".

Validator with options

Here an example, where we want to ask a new password to an user. This password should not be weak. The password will be asked twice and must match.

First of all, import necessary object:

from asyncio import run
import warnings
from re import match
from tiramisu import StrOption, IntOption, OptionDescription, Config, \
                     Calculation, Params, ParamOption, ParamSelfOption, ParamValue
from tiramisu.error import ValueWarning

Create a first function to valid that the password is not weak:

def is_password_conform(password):
    # password must containe at least a number, a lowercase letter, an uppercase letter and a symbol
    if not match(r'(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*\W)', password):
        raise ValueError('please choose a stronger password, try a mix of letters, numbers and symbols')

Secondly create a function to valid password length. The password must be longer than the value of "min_len" and should be longer than the value of "recommand_len".

In first case, function raise "ValueError", this value is incorrect.

In second case, function raise "ValueWarning", the value is valid but discouraged:

def password_correct_len(min_len, recommand_len, password):
    # password must have at least min_len characters
    if len(password) < min_len:
        raise ValueError(f'use {min_len} characters or more for your password')
    # password should have at least recommand_len characters
    if len(password) < recommand_len:
        raise ValueWarning(f'it would be better to use more than {recommand_len} characters for your password')

Thirdly create a function that verify that the login name is not a part of password (password "foo2aZ$" if not valid for user "foo"):

def user_not_in_password(login, password):
    if login in password:
        raise ValueError('the login must not be part of the password')

Now we can creation an option to ask user login:

login = StrOption('login', 'Login', properties=('mandatory',))

Create a calculation to launch "is_password_conform". This function will be use in a new option and must validate this new option. So we use the object "ParamSelfOption" has parameter to retrieve the value of current option:

calc1 = Calculation(is_password_conform,
                    Params(ParamSelfOption()))

Create a second calculation to launch "password_correct_len" function. We want set 8 as "min_len" value and 12 as "recommand_len" value:

calc2 = Calculation(password_correct_len,
                    Params((ParamValue(8),
                            ParamValue(12),
                            ParamSelfOption())))

Create a third calculation to launch "user_not_in_password" function. For this function, we use keyword argument. This function normaly raise "ValueError" but in this case we want demoting this error as a simple warning. So we add "warnings_only" parameter:

calc3 = Calculation(user_not_in_password,
                    Params(kwargs={'login': ParamOption(login),
                                   'password': ParamSelfOption()}),
                    warnings_only=True)

So now we can create first password option that use those calculations:

password1 = StrOption('password1',
                      'Password',
                      properties=('mandatory',),
                      validators=[calc1, calc2, calc3])

A new function is created to conform that password1 and password2 match:

def password_match(password1, password2):
    if password1 != password2:
        raise ValueError("those passwords didn't match, try again")

And now we can create second password option that use this function:

password2 = StrOption('password2',
                      'Confirm',
                      properties=('mandatory',),
                      validators=[Calculation(password_match, Params((ParamOption(password1), ParamSelfOption())))])

Finally we create optiondescription and config:

od = OptionDescription('password', 'Define your password', [password1, password2])
root = OptionDescription('root', '', [login, od])
async def main():
    config = await Config(root)
    await config.property.read_write()

run(main())

Now we can test this "Config" with a tested password too weak:

async def main():
    config = await Config(root)
    await config.property.read_write()
    await config.option('login').value.set('user')
    try:
        await config.option('password.password1').value.set('aAbBc')
    except ValueError as err:
        print(f'Error: {err}')

run(main())

returns:

Error: "aAbBc" is an invalid string for "Password", please choose a stronger password, try a mix of letters, numbers and symbols

The password is part of error message. In this case it's a bad idea. So we have to remove "prefix" to the error message:

async def main():
    config = await Config(root)
    await config.property.read_write()
    await config.option('login').value.set('user')
    try:
        await config.option('password.password1').value.set('aAbBc')
    except ValueError as err:
        err.prefix = ''
        print(f'Error: {err}')

run(main())

Now the error is:

Error: please choose a stronger password, try a mix of letters, numbers and symbols

Let's try with a password not weak but too short:

async def main():
    config = await Config(root)
    await config.property.read_write()
    await config.option('login').value.set('user')
    try:
        await config.option('password.password1').value.set('aZ$1')
    except ValueError as err:
        err.prefix = ''
        print(f'Error: {err}')

run(main())

The error is:

Error: use 8 characters or more for your password

Now try a password with 8 characters:

warnings.simplefilter('always', ValueWarning)
async def main():
    config = await Config(root)
    await config.property.read_write()
    await config.option('login').value.set('user')
    with warnings.catch_warnings(record=True) as warn:
        await config.option('password.password1').value.set('aZ$1bN:2')
        if warn:
            warn[0].message.prefix = ''
            print(f'Warning: {warn[0].message}')
        password = await config.option('password.password1').value.get()
    print(f'The password is "{password}"')

run(main())

Warning is display but password is store:

Warning: it would be better to use more than 12 characters for your password
The password is "aZ$1bN:2"

Try a password with the login as part of it:

warnings.simplefilter('always', ValueWarning)
async def main():
    config = await Config(root)
    await config.property.read_write()
    await config.option('login').value.set('user')
    with warnings.catch_warnings(record=True) as warn:
        await config.option('password.password1').value.set('aZ$1bN:2u@1Bjuser')
        if warn:
            warn[0].message.prefix = ''
            print(f'Warning: {warn[0].message}')
        password = await config.option('password.password1').value.get()
    print(f'The password is "{password}"')

run(main())

Warning is display but password is store:

Warning: the login must not be part of the password
The password is "aZ$1bN:2u@1Bjuser"

Now try with a valid password but that doesn't match:

async def main():
    config = await Config(root)
    await config.property.read_write()
    await config.option('login').value.set('user')
    await config.option('password.password1').value.set('aZ$1bN:2u@1Bj')
    try:
        await config.option('password.password2').value.set('aZ$1aaaa')
    except ValueError as err:
        err.prefix = ''
        print(f'Error: {err}')

run(main())

An error is displayed:

Error: those passwords didn't match, try again

Finally try a valid password:

async def main():
    config = await Config(root)
    await config.property.read_write()
    await config.option('login').value.set('user')
    await config.option('login').value.set('user')
    await config.option('password.password1').value.set('aZ$1bN:2u@1Bj')
    await config.option('password.password2').value.set('aZ$1bN:2u@1Bj')
    await config.property.read_only()
    user_login = await config.option('login').value.get()
    password = await config.option('password.password2').value.get()
    print(f'The password for "{user_login}" is "{password}"')

run(main())

As expected, we have:

The password for "user" is "aZ$1bN:2u@1Bj"

Validator with a multi option

Assume we ask percentage value to an user. The sum of values mustn't be higher than 100% and shouldn't be lower than 100%.

Let's start by importing the objects:

from asyncio import run
import warnings
from tiramisu import IntOption, OptionDescription, Config, \
                     Calculation, Params, ParamSelfOption
from tiramisu.error import ValueWarning

Continue by writing the validation function:

def valid_pourcent(option):
    total = sum(option)
    if total > 100:
        raise ValueError(f'the total {total}% is bigger than 100%')
    if total < 100:
        raise ValueWarning(f'the total {total}% is lower than 100%')

And create a simple option in a single OptionDescription:

percent = IntOption('percent',
                    'Percent',
                    multi=True,
                    validators=[Calculation(valid_pourcent, Params(ParamSelfOption()))])
od = OptionDescription('root', 'root', [percent])

Now try with bigger sum:

async def main():
    config = await Config(od)
    try:
        await config.option('percent').value.set([20, 90])
    except ValueError as err:
        err.prefix = ''
        print(f'Error: {err}')
    percent_value = await config.option('percent').value.get()
    print(f'The value is "{percent_value}"')

run(main())

The result is:

Error: the total 110% is bigger than 100%
The value is "[]"

Let's try with lower sum:

async def main():
    config = await Config(od)
    warnings.simplefilter('always', ValueWarning)
    with warnings.catch_warnings(record=True) as warn:
        await config.option('percent').value.set([20, 70])
        if warn:
            warn[0].message.prefix = ''
            print(f'Warning: {warn[0].message}')
        percent_value = await config.option('percent').value.get()
    print(f'The value is "{percent_value}"')

run(main())

The result is:

Warning: the total 90% is lower than 100%
The value is "[20, 70]"

Finally with correct value:

async def main():
    config = await Config(od)
    await config.option('percent').value.set([20, 80])
    percent_value = await config.option('percent').value.get()
    print(f'The value is "{percent_value}"')

run(main())

The result is:

The value is "[20, 80]"

Validator with a follower option

Assume we want distribute something to differents users. The sum of values mustn't be higher than 100%.

First, import all needed objects:

from asyncio import run
import warnings
from tiramisu import StrOption, IntOption, Leadership, OptionDescription, Config, \
                     Calculation, Params, ParamSelfOption, ParamIndex
from tiramisu.error import ValueWarning

Let's start to write a function with three arguments:

  • the first argument will have all values set for the follower
  • the second argument will have only last value set for the follower
  • the third argument will have the index
def valid_pourcent(option, current_option, index):
    if None in option:
        return
    total = sum(option)
    if total > 100:
        raise ValueError(f'the value {current_option} (at index {index}) is too big, the total is {total}%')

Continue by creating a calculation:

calculation = Calculation(valid_pourcent, Params((ParamSelfOption(whole=True),
                                                  ParamSelfOption(),
												  ParamIndex())))

And instanciate differents option:

user = StrOption('user', 'User', multi=True)
percent = IntOption('percent',
                    'Distribution',
                    multi=True,
                    validators=[calculation])
leader = Leadership('percent', 'Percent', [user, percent])
od = OptionDescription('root', 'root', [leader])

Add two values to the leader:

async def main():
    config = await Config(od)
    await config.option('percent.user').value.set(['user1', 'user2'])

run(main())

The user user1 will have 20%:

async def main():
    config = await Config(od)
    await config.option('percent.user').value.set(['user1', 'user2'])
    await config.option('percent.percent', 0).value.set(20)

run(main())

If we try to set 90% to user2:

async def main():
    config = await Config(od)
    await config.option('percent.user').value.set(['user1', 'user2'])
    await config.option('percent.percent', 0).value.set(20)
    try:
        await config.option('percent.percent', 1).value.set(90)
    except ValueError as err:
        err.prefix = ''
        print(f'Error: {err}')

run(main())

results:

Error: the value 90 (at index 1) is too big, the total is 110%

No problem with 80%:

async def main():
    config = await Config(od)
    await config.option('percent.user').value.set(['user1', 'user2'])
    await config.option('percent.percent', 0).value.set(20)
    await config.option('percent.percent', 1).value.set(80)

run(main())