495 lines
14 KiB
Markdown
495 lines
14 KiB
Markdown
|
# 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](calculation.md) 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:
|
||
|
|
||
|
```python
|
||
|
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:
|
||
|
|
||
|
```python
|
||
|
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:
|
||
|
|
||
|
```python
|
||
|
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"):
|
||
|
|
||
|
|
||
|
```python
|
||
|
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:
|
||
|
|
||
|
```python
|
||
|
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:
|
||
|
|
||
|
```python
|
||
|
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:
|
||
|
|
||
|
```python
|
||
|
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:
|
||
|
|
||
|
```python
|
||
|
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:
|
||
|
|
||
|
```python
|
||
|
password1 = StrOption('password1',
|
||
|
'Password',
|
||
|
properties=('mandatory',),
|
||
|
validators=[calc1, calc2, calc3])
|
||
|
```
|
||
|
|
||
|
A new function is created to conform that password1 and password2 match:
|
||
|
|
||
|
```python
|
||
|
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:
|
||
|
|
||
|
```python
|
||
|
password2 = StrOption('password2',
|
||
|
'Confirm',
|
||
|
properties=('mandatory',),
|
||
|
validators=[Calculation(password_match, Params((ParamOption(password1), ParamSelfOption())))])
|
||
|
```
|
||
|
|
||
|
Finally we create optiondescription and config:
|
||
|
|
||
|
```python
|
||
|
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:
|
||
|
|
||
|
```python
|
||
|
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:
|
||
|
|
||
|
```python
|
||
|
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:
|
||
|
|
||
|
```python
|
||
|
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:
|
||
|
|
||
|
```python
|
||
|
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:
|
||
|
|
||
|
```python
|
||
|
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:
|
||
|
|
||
|
```python
|
||
|
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:
|
||
|
|
||
|
```python
|
||
|
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:
|
||
|
|
||
|
```python
|
||
|
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:
|
||
|
|
||
|
```python
|
||
|
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:
|
||
|
|
||
|
```python
|
||
|
percent = IntOption('percent',
|
||
|
'Percent',
|
||
|
multi=True,
|
||
|
validators=[Calculation(valid_pourcent, Params(ParamSelfOption()))])
|
||
|
od = OptionDescription('root', 'root', [percent])
|
||
|
```
|
||
|
|
||
|
Now try with bigger sum:
|
||
|
|
||
|
```python
|
||
|
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:
|
||
|
|
||
|
```python
|
||
|
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:
|
||
|
|
||
|
```python
|
||
|
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:
|
||
|
|
||
|
```python
|
||
|
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
|
||
|
|
||
|
```python
|
||
|
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:
|
||
|
|
||
|
```python
|
||
|
calculation = Calculation(valid_pourcent, Params((ParamSelfOption(whole=True),
|
||
|
ParamSelfOption(),
|
||
|
ParamIndex())))
|
||
|
```
|
||
|
|
||
|
And instanciate differents option:
|
||
|
|
||
|
```python
|
||
|
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:
|
||
|
|
||
|
```python
|
||
|
async def main():
|
||
|
config = await Config(od)
|
||
|
await config.option('percent.user').value.set(['user1', 'user2'])
|
||
|
|
||
|
run(main())
|
||
|
```
|
||
|
|
||
|
The user user1 will have 20%:
|
||
|
|
||
|
```python
|
||
|
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:
|
||
|
|
||
|
```python
|
||
|
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%:
|
||
|
|
||
|
```python
|
||
|
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())
|
||
|
```
|