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:

1from tiramisu import StrOption, IntOption, OptionDescription, Config, \
2                     Calculation, Params, ParamOption, ParamSelfOption, ParamValue
3from tiramisu.error import ValueWarning
4import warnings
5from re import match

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

1# Creation differents function
2def is_password_conform(password):
3    # password must containe at least a number, a lowercase letter, an uppercase letter and a symbol
4    if not match(r'(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*\W)', password):
5        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:

1# Password must have at least min_len characters
2def password_correct_len(min_len, recommand_len, password):
3    if len(password) < min_len:
4        raise ValueError(f'use {min_len} characters or more for your password')
5    # password should have at least recommand_len characters
6    if len(password) < recommand_len:
7        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):

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

Now we can creation an option to ask user login:

1# Create first option to ask user's login
2login = 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:

1# Creation calculatin for first password
2calc1 = Calculation(is_password_conform,
3                    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:

1calc2 = Calculation(password_correct_len,
2                    Params((ParamValue(8),
3                            ParamValue(12),
4                            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:

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

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

1# Create second option to ask user's password
2password1 = StrOption('password1',
3                      'Password',
4                      properties=('mandatory',),
5                      validators=[calc1, calc2, calc3])

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

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

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

1# Create third option to confirm user's password
2password2 = StrOption('password2',
3                      'Confirm',
4                      properties=('mandatory',),
5                      validators=[Calculation(password_match, Params((ParamOption(password1), ParamSelfOption())))])

Finally we create optiondescription and config:

1# Creation optiondescription and config
2od = OptionDescription('password', 'Define your password', [password1, password2])
3root = OptionDescription('root', '', [login, od])
4config = Config(root)
5config.property.read_write()

Now we can test this Config:

1# no number and no symbol (with prefix)
2config.option('login').value.set('user')
3try:
4    config.option('password.password1').value.set('aAbBc')
5except ValueError as err:
6    print(f'Error: {err}')

The tested password is too weak, so value is not set. The error is: 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:

1# no number and no symbol
2config.option('login').value.set('user')
3try:
4    config.option('password.password1').value.set('aAbBc')
5except ValueError as err:
6    err.prefix = ''
7    print(f'Error: {err}')

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:

1# too short password
2config.option('login').value.set('user')
3try:
4    config.option('password.password1').value.set('aZ$1')
5except ValueError as err:
6    err.prefix = ''
7    print(f'Error: {err}')

The error is: Error: use 8 characters or more for your password.

Now try a password with 8 characters:

 1# warnings too short password
 2warnings.simplefilter('always', ValueWarning)
 3config.option('login').value.set('user')
 4with warnings.catch_warnings(record=True) as warn:
 5    config.option('password.password1').value.set('aZ$1bN:2')
 6    if warn:
 7        warn[0].message.prefix = ''
 8        print(f'Warning: {warn[0].message}')
 9    password = config.option('password.password1').value.get()
10print(f'The password is "{password}"')

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:

 1# password with login
 2warnings.simplefilter('always', ValueWarning)
 3config.option('login').value.set('user')
 4with warnings.catch_warnings(record=True) as warn:
 5    config.option('password.password1').value.set('aZ$1bN:2u@1Bjuser')
 6    if warn:
 7        warn[0].message.prefix = ''
 8        print(f'Warning: {warn[0].message}')
 9    password = config.option('password.password1').value.get()
10print(f'The password is "{password}"')

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:

1# password1 not matching password2
2config.option('login').value.set('user')
3config.option('password.password1').value.set('aZ$1bN:2u@1Bj')
4try:
5    config.option('password.password2').value.set('aZ$1aaaa')
6except ValueError as err:
7    err.prefix = ''
8    print(f'Error: {err}')

An error is displayed: Error: those passwords didn't match, try again.

Finally try a valid password:

1# and finaly passwod match
2config.option('login').value.set('user')
3config.option('password.password1').value.set('aZ$1bN:2u@1Bj')
4config.option('password.password2').value.set('aZ$1bN:2u@1Bj')
5config.property.read_only()
6user_login = config.option('login').value.get()
7password = config.option('password.password2').value.get()
8print(f'The password for "{user_login}" is "{password}"')

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:

1from tiramisu import IntOption, OptionDescription, Config, \
2                     Calculation, Params, ParamSelfOption
3from tiramisu.error import ValueWarning
4import warnings

Continue by writing the validation function:

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

And create a simple config:

1percent = IntOption('percent',
2                    'Percent',
3                    multi=True,
4                    validators=[Calculation(valid_pourcent, Params(ParamSelfOption()))])
5config = Config(OptionDescription('root', 'root', [percent]))

Now try with bigger sum:

1# too big
2try:
3    config.option('percent').value.set([20, 90])
4except ValueError as err:
5    err.prefix = ''
6    print(f'Error: {err}')
7percent_value = config.option('percent').value.get()
8print(f'The value is "{percent_value}"')

The result is:

Error: the total 110% is bigger than 100%

The value is "[]"

Let’s try with lower sum:

1# too short
2warnings.simplefilter('always', ValueWarning)
3with warnings.catch_warnings(record=True) as warn:
4    config.option('percent').value.set([20, 70])
5    if warn:
6        warn[0].message.prefix = ''
7        print(f'Warning: {warn[0].message}')
8    percent_value = config.option('percent').value.get()
9print(f'The value is "{percent_value}"')

The result is:

Warning: the total 90% is lower than 100%

The value is "[20, 70]"

Finally with correct value:

1# correct
2config.option('percent').value.set([20, 80])
3percent_value = config.option('percent').value.get()
4print(f'The value is "{percent_value}"')

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:

1from tiramisu import StrOption, IntOption, Leadership, OptionDescription, Config, \
2                     Calculation, Params, ParamSelfOption, ParamIndex
3from tiramisu.error import ValueWarning
4import warnings

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

1def valid_pourcent(option, current_option, index):
2    if None in option:
3        return
4    total = sum(option)
5    if total > 100:
6        raise ValueError(f'the value {current_option} (at index {index}) is too big, the total is {total}%')

Continue by creating a calculation:

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

And instanciate differents option and config:

1user = StrOption('user', 'User', multi=True)
2percent = IntOption('percent',
3                    'Distribution',
4                    multi=True,
5                    validators=[calculation])
6od = Leadership('percent', 'Percent', [user, percent])
7config = Config(OptionDescription('root', 'root', [od]))

Add two value to the leader:

1config.option('percent.user').value.set(['user1', 'user2'])

The user user1 will have 20%:

1config.option('percent.percent', 0).value.set(20)

If we try to set 90% to user2:

1# too big
2try:
3    config.option('percent.percent', 1).value.set(90)
4except ValueError as err:
5    err.prefix = ''
6    print(f'Error: {err}')

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

No problem with 80%:

1# correct
2config.option('percent.percent', 1).value.set(80)