rst to md doc conversion

This commit is contained in:
egarette@silique.fr 2022-11-13 15:04:12 +01:00
parent 026e665ab0
commit ece7537b89
19 changed files with 2721 additions and 9 deletions

View file

@ -1,5 +1,9 @@
LICENSES ![Logo Tiramisu](logo.png "logo Tiramisu")
---------
[Documentations](doc/README.md)
# LICENSES
See COPYING for the licences of the code and the documentation. See COPYING for the licences of the code and the documentation.

64
doc/README.md Normal file
View file

@ -0,0 +1,64 @@
![Logo Tiramisu](../logo.png "logo Tiramisu")
# Python3 Tiramisu library user documentation
## The tasting of `Tiramisu` --- `user documentation`
Tiramisu:
- is a cool, refreshing Italian dessert,
- it is also an [options controller tool](http://en.wikipedia.org/wiki/Configuration_management#Overview)
It's a pretty small, local (that is, straight on the operating system) options handler and controller.
- [Getting started](gettingstarted.md)
- [The Config](config.md)
- [Browse the Config](browse.md)
- [Manage values](api_value.md)
.. toctree::
:maxdepth: 2
api_property
storage
application
quiz
glossary
External project:
.. toctree::
:maxdepth: 2
cmdline_parser
.. FIXME ca veut rien dire : "AssertionError: type <class 'tiramisu.autolib.Calculation'> invalide pour des propriétés pour protocols, doit être un frozenset"
.. FIXME changer le display_name !
.. FIXME voir si warnings_only dans validator !
.. FIXME submulti dans les leadership
.. FIXME exemple avec default_multi (et undefined)
.. FIXME config, metaconfig, ...
.. FIXME fonction de base
.. FIXME information
.. FIXME demoting_error_warning, warnings, ...
.. FIXME class _TiramisuOptionOptionDescription(CommonTiramisuOption):
.. FIXME class _TiramisuOptionOption(_TiramisuOptionOptionDescription):
.. FIXME class TiramisuOptionInformation(CommonTiramisuOption):
.. FIXME class TiramisuContextInformation(TiramisuConfig):
.. FIXME expire
.. FIXME custom display_name
.. FIXME assert await cfg.cache.get_expiration_time() == 5
.. FIXME await cfg.cache.set_expiration_time(1)
.. FIXME convert_suffix_to_path
Indices and full bunch of code
===============================
* `All files for which code is available <_modules/index.html>`_
* :ref:`genindex`
* :ref:`search`

439
doc/api_value.md Normal file
View file

@ -0,0 +1,439 @@
# Manage values
## Values with options
### Simple option
Begin by creating a Config. This Config will contains two options:
- first one is an option where the user will set an unix path
- second one is an option that calculate the disk usage of the previous unix path
Let's import needed object:
```python
from asyncio import run
from shutil import disk_usage
from os.path import isdir
from tiramisu import FilenameOption, FloatOption, OptionDescription, Config, \
Calculation, Params, ParamValue, ParamOption, ParamSelfOption
```
Create a function that verify the path exists in current system:
```python
def valid_is_dir(path):
# verify if path is a directory
if not isdir(path):
raise ValueError('this directory does not exist')
```
Use this function as a :doc:`validator` in a new option call `path`:
```python
filename = FilenameOption('path', 'Path', validators=[Calculation(valid_is_dir,
Params(ParamSelfOption()))])
```
Create a second function that calculate the disk usage:
```python
def calc_disk_usage(path, size='bytes'):
# do not calc if path is None
if path is None:
return None
if size == 'bytes':
div = 1
else:
# bytes to gigabytes
div = 1024 * 1024 * 1024
return disk_usage(path).free / div
```
Add a new option call `usage` that use this function with first argument the option `path` created before:
```python
usage = FloatOption('usage', 'Disk usage', Calculation(calc_disk_usage,
Params(ParamOption(filename))))
```
Finally add those options in option description and a Config:
```
disk = OptionDescription('disk', 'Verify disk usage', [filename, usage])
root = OptionDescription('root', 'root', [disk])
async def main():
config = await Config(root)
await config.property.read_write()
config = run(main())
```
#### Get and set a value
First of all, retrieve the values of both options:
```python
async def main():
print(await config.option('disk.path').value.get())
print(await config.option('disk.usage').value.get())
run(main())
```
returns:
```
None
None
```
Enter a value of the `path` option:
```python
async def main():
await config.option('disk.path').value.set('/')
print(await config.option('disk.path').value.get())
print(await config.option('disk.usage').value.get())
run(main())
```
returns:
```
/
668520882176.0
```
When you enter a value it is validated:
>>> try:
>>> config.option('disk.path').value.set('/unknown')
>>> except ValueError as err:
>>> print(err)
"/unknown" is an invalid file name for "Path", this directory does not exist
We can also set a :doc:`calculation` as value. For example, we want to launch previous function but with in_gb to True as second argument:
>>> calc = Calculation(calc_disk_usage, Params((ParamOption(filename),
... ParamValue('gigabytes'))))
>>> config.option('disk.usage').value.set(calc)
>>> config.option('disk.usage').value.get()
622.6080360412598
#### Is value is valid?
To check is a value is valid:
>>> config.option('disk.path').value.valid()
True
#### Display the default value
Even if the value is modify, you can display the default value with `default` method:
>>> config.option('disk.path').value.set('/')
>>> config.option('disk.usage').value.set(1.0)
>>> config.option('disk.usage').value.get()
1.0
>>> config.option('disk.usage').value.default()
668510105600.0
#### Return to the default value
If the value is modified, just `reset` it to retrieve the default value:
>>> config.option('disk.path').value.set('/')
>>> config.option('disk.path').value.get()
/
>>> config.option('disk.path').value.reset()
>>> config.option('disk.path').value.get()
None
#### The ownership of a value
Every option has an owner, that will indicate who changed the option's value last.
The default owner of every option is "default", and means that the value is the default one.
If you use a "reset" instruction to get back to the default value, the owner will get back
to "default" as well.
>>> config.option('disk.path').value.reset()
>>> config.option('disk.path').owner.isdefault()
True
>>> config.option('disk.path').owner.get()
default
>>> config.option('disk.path').value.set('/')
>>> config.option('disk.path').owner.isdefault()
False
>>> config.option('disk.path').owner.get()
user
All modified values have an owner. We can change at anytime this owner:
>>> config.option('disk.path').owner.set('itsme')
>>> config.option('disk.path').owner.get()
itsme
.. note::
This will work only if the current owner isn't "default".
This new user will be keep until anyone change the value:
>>> config.option('disk.path').value.set('/')
>>> config.option('disk.path').owner.get()
user
This username is in fact the `config` user, which is `user` by default:
>>> config.owner.get()
user
This owner will be the owner that all the options in the config will get when their value is changed.
This explains why earlier, the owner became "user" when changing the option's value.
We can change this owner:
>>> config.owner.set('itsme')
>>> config.option('disk.path').value.set('/')
>>> config.option('disk.path').owner.get()
itsme
### Get choices from a Choice option
In the previous example, it's difficult to change the second argument of the `calc_disk_usage`.
For ease the change, add a `ChoiceOption` and replace the `size_type` and `disk` option:
.. literalinclude:: ../src/api_value_choice.py
:lines: 26-31
:linenos:
We set the default value to `bytes`, if not, the default value will be None.
:download:`download the config <../src/api_value_choice.py>`
At any time, we can get all de choices avalaible for an option:
>>> config.option('disk.size_type').value.list()
('bytes', 'giga bytes')
### Value in multi option
.. FIXME undefined
For multi option, just modify a little bit the previous example.
The user can, now, set multiple path.
First of all, we have to modification in this option:
- add multi attribute to True
- the function use in validation valid a single value, so each value in the list must be validate separatly, for that we add whole attribute to False in `ParamSelfOption` object
.. literalinclude:: ../src/api_value_multi.py
:lines: 23-25
:linenos:
Secondly, the function calc_disk_usage must return a list:
.. literalinclude:: ../src/api_value_multi.py
:lines: 11-26
:linenos:
Finally `usage` option is also a multi:
.. literalinclude:: ../src/api_value_multi.py
:lines: 27-30
:linenos:
:download:`download the config <../src/api_value_multi.py>`
#### Get or set a multi value
Since the options are multi, the default value is a list:
>>> config.option('disk.path').value.get()
[]
>>> config.option('disk.usage').value.get()
[]
A multi option waiting for a list:
>>> config.option('disk.path').value.set(['/', '/tmp'])
>>> config.option('disk.path').value.get()
['/', '/tmp']
>>> config.option('disk.usage').value.get()
[668499898368.0, 8279277568.0]
#### The ownership of multi option
There is no difference in behavior between a simple option and a multi option:
>>> config.option('disk.path').value.reset()
>>> config.option('disk.path').owner.isdefault()
True
>>> config.option('disk.path').owner.get()
default
>>> config.option('disk.path').value.set(['/', '/tmp'])
>>> config.option('disk.path').owner.get()
user
### Leadership
In previous example, we cannot define different `size_type` for each path. If you want do this, you need a leadership.
In this case, each time we add a path, we can change an associate `size_type`.
As each value of followers are isolate, the function `calc_disk_usage` will receive only one path and one size.
So let's change this function:
.. literalinclude:: ../src/api_value_leader.py
:lines: 12-18
:linenos:
Secondly the option `size_type` became a multi:
.. literalinclude:: ../src/api_value_leader.py
:lines: 24-25
:linenos:
Finally disk has to be a leadership:
.. literalinclude:: ../src/api_value_leader.py
:lines: 30
:linenos:
#### Get and set a leader
A leader is, in fact, a multi option:
>>> config.option('disk.path').value.set(['/', '/tmp'])
>>> config.option('disk.path').value.get()
['/', '/tmp']
There is two differences:
- we can get the leader length:
>>> config.option('disk.path').value.set(['/', '/tmp'])
>>> config.option('disk.path').value.len()
2
- we cannot reduce by assignation a leader:
>>> config.option('disk.path').value.set(['/', '/tmp'])
>>> from tiramisu.error import LeadershipError
>>> try:
... config.option('disk.path').value.set(['/'])
... except LeadershipError as err:
... print(err)
cannot reduce length of the leader "Path"
We cannot reduce a leader because Tiramisu cannot determine which isolate follower we have to remove, this first one or the second one?
To reduce use the `pop` method:
>>> config.option('disk.path').value.set(['/', '/tmp'])
>>> config.option('disk.path').value.pop(1)
>>> config.option('disk.path').value.get()
['/']
#### Get and set a follower
As followers are isolate, we cannot get all the follower values:
>>> config.option('disk.path').value.set(['/', '/tmp'])
>>> from tiramisu.error import APIError
>>> try:
... config.option('disk.size_type').value.get()
... except APIError as err:
... print(err)
index must be set with the follower option "Size type"
Index is mandatory:
>>> config.option('disk.path').value.set(['/', '/tmp'])
>>> config.option('disk.size_type', 0).value.get()
bytes
>>> config.option('disk.size_type', 1).value.get()
bytes
It's the same thing during the assignment:
>>> config.option('disk.path').value.set(['/', '/tmp'])
>>> config.option('disk.size_type', 0).value.set('giga bytes')
As the leader, follower has a length (in fact, this is the leader's length):
>>> config.option('disk.size_type').value.len()
2
#### The ownership of a leader and follower
There is no differences between a multi option and a leader option:
>>> config.option('disk.path').value.set(['/', '/tmp'])
>>> config.option('disk.path').owner.get()
user
For follower, it's different, always because followers are isolate:
>>> config.option('disk.size_type', 0).value.set('giga bytes')
>>> config.option('disk.size_type', 0).owner.isdefault()
False
>>> config.option('disk.size_type', 0).owner.get()
user
>>> config.option('disk.size_type', 1).owner.isdefault()
True
>>> config.option('disk.size_type', 1).owner.get()
default
## Values in option description
With an option description we can have directly a dict with all option's name and value:
>>> config.option('disk.path').value.set(['/', '/tmp'])
>>> config.option('disk.size_type', 0).value.set('giga bytes')
>>> config.option('disk').value.dict()
{'path': ['/', '/tmp'], 'size_type': ['giga bytes', 'bytes'], 'usage': [622.578239440918, 8279273472.0]}
An attribute fullpath permit to have fullpath of child option:
>>> config.option('disk').value.dict(fullpath=True)
{'disk.path': ['/', '/tmp'], 'disk.size_type': ['giga bytes', 'bytes'], 'disk.usage': [622.578239440918, 8279273472.0]}
## Values in config
###dict
With the `config` we can have directly a dict with all option's name and value:
>>> config.option('disk.path').value.set(['/', '/tmp'])
>>> config.option('disk.size_type', 0).value.set('giga bytes')
>>> config.value.dict()
{'disk.path': ['/', '/tmp'], 'disk.size_type': ['giga bytes', 'bytes'], 'disk.usage': [622.578239440918, 8279273472.0]}
If you don't wan't path but only the name:
>>> config.value.dict(flatten=True)
{'path': ['/', '/tmp'], 'size_type': ['giga bytes', 'bytes'], 'usage': [622.578239440918, 8279273472.0]}
### importation/exportation
In config, we can export full values:
>>> config.value.exportation()
[['disk.path', 'disk.size_type'], [None, [0]], [['/', '/tmp'], ['giga bytes']], ['user', ['user']]]
and reimport it later:
>>> export = config.value.exportation()
>>> config.value.importation(export)
.. note:: The exportation format is not stable and can be change later, please do not use importation otherwise than jointly with exportation.

350
doc/browse.md Normal file
View file

@ -0,0 +1,350 @@
# Browse the Config
## Getting the options
Create a simple Config:
```python
from asyncio import run
from tiramisu import Config
from tiramisu import StrOption, OptionDescription
# let's declare some options
var1 = StrOption('var1', 'first option')
# an option with a default value
var2 = StrOption('var2', 'second option', 'value')
# let's create a group of options
od1 = OptionDescription('od1', 'first OD', [var1, var2])
# let's create another group of options
rootod = OptionDescription('rootod', '', [od1])
async def main():
# let's create the config
cfg = await Config(rootod)
# the api is read only
await cfg.property.read_only()
# the read_write api is available
await cfg.property.read_write()
return cfg
cfg = run(main())
```
We retrieve by path an option named "var1" and then we retrieve its name and its docstring:
```python
async def main():
print(await cfg.option('od1.var1').option.name())
print(await cfg.option('od1.var1').option.doc())
run(main())
```
returns:
```
var1
first option
```
## Accessing the values of the options
Let's browse the configuration structure and option values.
You have getters as a "get" method on option objects:
1. getting all the options
```python
async def main():
print(await cfg.value.dict())
run(main())
```
returns:
```
{'var1': None, 'var2': 'value'}
```
2. getting the "od1" option description
```python
async def main():
print(await cfg.option('od1').value.dict())
run(main())
```
returns:
```
{'od1.var1': None, 'od1.var2': 'value'}
```
3. getting the var1 option's value
```python
async def main():
print(await cfg.option('od1.var1').value.get())
run(main())
```
returns:
```
None
```
4. getting the var2 option's default value
```python
async def main():
print(await cfg.option('od1.var2').value.get())
run(main())
```
returns:
```
value
```
5. trying to get a non existent option's value
```python
async def main():
try:
await cfg.option('od1.idontexist').value.get()
except AttributeError as err:
print(str(err))
run(main())
```
returns:
```
unknown option "idontexist" in optiondescription "first OD"
```
## Setting the value of an option
An important part of the setting's configuration consists of setting the
value's option.
You have setters as a "set" method on option objects.
And if you wanna come back to a default value, use the "reset()" method.
1. changing the "od1.var1" value
```python
async def main():
await cfg.option('od1.var1').value.set('éééé')
print(await cfg.option('od1.var1').value.get())
run(main())
```
returns:
```
éééé
```
2. carefull to the type of the value to be set
```python
async def main():
try:
await cfg.option('od1.var1').value.set(23454)
except ValueError as err:
print(str(err))
run(main())
```
returns:
```
"23454" is an invalid string for "first option"
```
3. let's come back to the default value
```python
async def main():
await cfg.option('od1.var2').value.reset()
print(await cfg.option('od1.var2').value.get())
run(main())
```
returns:
```
value
```
> **_Important_** If the config is "read only", setting an option's value isn't allowed, see [property](property.md).
Let's make the protocol of accessing a Config's option explicit
(because explicit is better than implicit):
1. If the option has not been declared, an "Error" is raised,
2. If an option is declared, but neither a value nor a default value has
been set, the returned value is "None",
3. If an option is declared and a default value has been set, but no value
has been set, the returned value is the default value of the option,
4. If an option is declared, and a value has been set, the returned value is
the value of the option.
But there are special exceptions. We will see later on that an option can be a
mandatory option. A mandatory option is an option that must have a value
defined.
Searching for an option
~~~~~~~~~~~~~~~~~~~~~~~~~~
In an application, knowing the path of an option is not always feasible.
That's why a tree of options can easily be searched with the "find()" method.
```python
from asyncio import run
from tiramisu import Config
from tiramisu import OptionDescription, StrOption
from tiramisu.setting import undefined
var1 = StrOption('var1', '')
var2 = StrOption('var2', '')
var3 = StrOption('var3', '')
od1 = OptionDescription('od1', '', [var1, var2, var3])
var4 = StrOption('var4', '')
var5 = StrOption('var5', '')
var6 = StrOption('var6', '')
var7 = StrOption('var1', '', 'value')
od2 = OptionDescription('od2', '', [var4, var5, var6, var7])
rootod = OptionDescription('rootod', '', [od1, od2])
```
Let's find an option by it's name
And let's find first an option by it's name
The search can be performed in a subtree
```python
async def main():
cfg = await Config(rootod)
print(await cfg.option.find(name='var1'))
run(main())
```
returns:
```
[<tiramisu.api.TiramisuOption object at 0x7f490a530f98>, <tiramisu.api.TiramisuOption object at 0x7f490a530748>]
```
If the option name is unique, the search can be stopped once one matched option has been found:
```python
async def main():
cfg = await Config(rootod)
print(await cfg.option.find(name='var1', first=True))
run(main())
```
returns:
```
<tiramisu.api.TiramisuOption object at 0x7ff27fc93c70>
```
Search object behaves like a cfg object, for example:
```python
async def main():
cfg = await Config(rootod)
option = await cfg.option.find(name='var1', first=True)
print(await option.option.name())
print(await option.option.doc())
run(main())
```
returns:
```
var1
var1
```
Search can be made with various criteria:
```python
async def main():
cfg = await Config(rootod)
await cfg.option.find(name='var3', value=undefined)
await cfg.option.find(name='var3', type=StrOption)
run(main())
```
The find method can be used in subconfigs:
```python
async def main():
cfg = await Config(rootod)
print(await cfg.option('od2').find('var1'))
run(main())
```
## The "dict" flattening utility
In a config or a subconfig, you can print a dict-like representation
In a "fullpath" or a "flatten" way
- get the "od1" option description:
```python
async def main():
cfg = await Config(rootod)
print(await cfg.option('od1').value.dict(fullpath=True))
run(main())
```
returns:
```
{'od1.var1': None, 'od1.var2': None, 'od1.var3': None}
```
```python
async def main():
cfg = await Config(rootod)
print(await cfg.option('od1').value.dict(fullpath=False))
run(main())
>>> print(cfg.option('od1').value.dict(fullpath=True))
```
returns:
```
{'var1': None, 'var2': None, 'var3': None}
```
> **_NOTE_** be carefull with this "flatten" parameter, because we can just loose some options if there are same name (some option can be overriden).

107
doc/config.md Normal file
View file

@ -0,0 +1,107 @@
# The Config
Tiramisu is made of almost three main classes/concepts :
- the "Option" stands for the option types
- the "OptionDescription" is the schema, the option's structure
- the "Config" which is the whole configuration entry point
![The Config](config.png "The Config")
## The handling of options
The handling of options is split into two parts: the description of
which options are available, what their possible values and defaults are
and how they are organized into a tree. A specific choice of options is
bundled into a configuration object which has a reference to its option
description (and therefore makes sure that the configuration values
adhere to the option description).
- [Instanciate an option](option.md)
- [Default Options](options.md)
- [The symbolic link option: SymLinkOption](symlinkoption.md)
- [Create it's own option](own_option.md)
## Option description are nested Options
The Option (in this case the "BoolOption"),
are organized into a tree into nested "OptionDescription" objects.
Every option has a name, as does every option group.
- [Generic container: OptionDescription](optiondescription.md)
- [Dynamic option description: DynOptionDescription](dynoptiondescription.md)
- [Leadership OptionDescription: Leadership](leadership.md)
## Config
Getting started with the tiramisu library (it loads and prints properly).
Let's perform a *Getting started* code review :
```python
from asyncio import run
from tiramisu import Config
from tiramisu import OptionDescription, BoolOption
# let's create a group of options named "optgroup"
descr = OptionDescription("optgroup", "", [
# ... with only one option inside
BoolOption("bool", "", default=False)
])
async def main():
# Then, we make a Config with the OptionDescription` we
cfg = await Config(descr)
# the global help about the config
cfg.help()
run(main())
```
returns:
```
Root config object that enables us to handle the configuration options
Settings:
forcepermissive Access to option without verifying permissive properties
unrestraint Access to option without property restriction
Commands:
cache Manage config cache
config Actions to Config
information Manage config informations
option Select an option
owner Global owner
permissive Manage config permissives
property Manage config properties
session Manage Config session
value Manage config value
```
Then let's print our "Option details.
```python
cfg.option.help()
```
returns:
```
Select an option
Call: Select an option by path
Commands:
dict Convert config and option to tiramisu format
find Find an or a list of options
list List options (by default list only option)
updates Updates value with tiramisu format
```
## Go futher with "Option" and "Config"
- [property](property.md)
- [validator](validator.md)

BIN
doc/config.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,57 @@
# Dynamic option description: DynOptionDescription
## Dynamic option description's description
Dynamic option description is an OptionDescription which multiplies according to the return of a function.
First of all, an option description is a name, a description, children and suffixes.
### name
The "name" is important to retrieve this dynamic option description.
### description
The "description" allows the user to understand where this dynamic option description will contains.
### children
List of children option.
> **_NOTE:_** the option has to be multi option or leadership but not option description.
### suffixes
Suffixes is a [calculation](calculation.md) that return the list of suffixes used to create dynamic option description.
Let's try:
```python
from tiramisu import StrOption, DynOptionDescription, Calculation
def return_suffixes():
return ['1', '2']
child1 = StrOption('first', 'First basic option ')
child2 = StrOption('second', 'Second basic option ')
DynOptionDescription('basic ',
'Basic options ',
[child1, child2],
Calculation(return_suffixes))
```
This example will construct:
- Basic options 1:
- First basic option 1
- Second basic option 1
- Basic options 2:
- First basic option 2
- Second basic option 2
## Dynamic option description's properties
See [property](property.md).

42
doc/gettingstarted.md Normal file
View file

@ -0,0 +1,42 @@
# Getting started
## What is options handling ?
Due to more and more available options required to set up an operating system,
compiler options or whatever, it became quite annoying to hand the necessary
options to where they are actually used and even more annoying to add new
options.
To circumvent these problems the configuration control was introduced.
## What is Tiramisu ?
Tiramisu is an options handler and an options controller, which aims at
producing flexible and fast options access. The main advantages are its access
rules and the fact that the whole consistency is preserved at any time.
There is of course type and structure validations, but also
validations towards the whole options. Furthermore, options can be reached and
changed according to the access rules from nearly everywhere.
### Installation
The best way is to use the python [pip](https://pip.pypa.io/en/stable/installing/) installer
And then type:
```bash
$ pip install tiramisu
```
### Advanced users
To obtain a copy of the sources, check it out from the repository using `git`.
We suggest using `git` if one wants to access to the current developments.
```bash
$ git clone https://framagit.org/tiramisu/tiramisu.git
```
This will get you a fresh checkout of the code repository in a local directory
named "tiramisu".

45
doc/leadership.md Normal file
View file

@ -0,0 +1,45 @@
# Leadership OptionDescription: Leadership
A leadership is a special "OptionDescription" that wait a leader and one or multiple followers.
Leader and follower are multi option. The difference is that the length is defined by the length of the option leader.
If the length of leader is 3, all followers have also length 3.
An other different is that the follower is isolate. That means that you can only change on value on a specified index in the list of it's values.
If a value is mark as modified in a specified index, that not affect the other values in other index.
## The leadership's description
A leadership is an "OptionDescription"
First of all, an option leadership is a name, a description and children.
### name
The "name" is important to retrieve this dynamic option description.
### description
The "description" allows the user to understand where this dynamic option description will contains.
### children
List of children option.
> **_NOTE:_** the option has to be multi or submulti option and not other option description.
Let's try:
```python
from tiramisu import StrOption, Leadership
users = StrOption('users', 'User', multi=True)
passwords = StrOption('passwords', 'Password', multi=True)
Leadership('users',
'User allow to connect',
[users, passwords])
```
## The leadership's properties
See [property](property.md).

134
doc/option.md Normal file
View file

@ -0,0 +1,134 @@
# Instanciate an option
## Option's description
First of all, an option is a name and a description.
### name
The "name" is important to retrieve this option.
### description
The "description" allows the user to understand where this option will be used for.
Let's try:
```python
from tiramisu import StrOption
StrOption('welcome',
'Welcome message to the user login')
```
## Option's default value
For each option, we can defined a default value. This value will be the value of this option until user customize it.
This default value is store directly in the option. So we can, at any moment we can go back to the default value.
```python
StrOption('welcome',
'Welcome message to the user login',
'Hey guys, welcome here!')
```
The default value can be a calculation.
```python
from tiramisu import Calculation
def get_value():
return 'Hey guys, welcome here'
StrOption('welcome',
'Welcome message to the user login',
Calculation(get_value))
```
## Option with multiple value
### multi
There are cases where it can be interesting to have a list of values rather than just one.
The "multi" attribut is here for that.
In this case, the default value has to be a list:
```python
StrOption('shopping_list',
'The shopping list',
['1 kilogram of carrots', 'leeks', '1 kilogram of potatos'],
multi=True)
```
The option could be a list of list, which is could submulti:
```python
from tiramisu import submulti
StrOption('shopping_list',
'The shopping list',
[['1 kilogram of carrots', 'leeks', '1 kilogram of potatos'], ['milk', 'eggs']],
multi=submulti)
```
The default value can be a calculation. For a multi, the function have to return a list or have to be in a list:
```python
def get_values():
return ['1 kilogram of carrots', 'leeks', '1 kilogram of potatos']
StrOption('shopping_list',
'The shopping list',
Calculation(get_values),
multi=True)
```
```python
def get_a_value():
return 'leeks'
StrOption('shopping_list',
'The shopping list',
['1 kilogram of carrots', Calculation(get_a_value), '1 kilogram of potatos'],
multi=True)
```
### default_multi
A second default value is available for multi option, "default_multi". This value is used when we add new value without specified a value.
This "default_multi" must not be a list in multi purpose. For submulti, it has to be a list:
```python
StrOption('shopping_list',
'The shopping list',
['1 kilogram of carrots', 'leeks', '1 kilogram of potatos'],
default_multi='some vegetables',
multi=True)
StrOption('shopping_list',
'The shopping list',
[['1 kilogram of carrots', 'leeks', '1 kilogram of potatos'], ['milk', 'eggs']],
default_multi=['some', 'vegetables'],
multi=submulti)
```
The default_multi value can be a calculation:
```python
def get_a_value():
return 'some vegetables'
StrOption('shopping_list',
'The shopping list',
['1 kilogram of carrots', 'leeks', '1 kilogram of potatos'],
default_multi=Calculation(get_a_value),
multi=True)
```
## Other option's parameters
There are two other parameters.
We will see them later:
- [Property](property.md)
- [Validator](validator.md)

35
doc/optiondescription.md Normal file
View file

@ -0,0 +1,35 @@
# Generic container: OptionDescription
## Option description's description
First of all, an option description is a name, a description and children.
### name
The "name" is important to retrieve this option description.
### description
The "description" allows the user to understand where this option description will contains.
### children
List of children option.
> **_NOTE:_** the option can be an option or an other option description
Let's try:
```python
from tiramisu import StrOption, OptionDescription
child1 = StrOption('first', 'First basic option')
child2 = StrOption('second', 'Second basic option')
OptionDescription('basic',
'Basic options',
[child1, child2])
```
## Option description's properties
See [property](property.md).

485
doc/options.md Normal file
View file

@ -0,0 +1,485 @@
# Default Options
Basic options
## Textual option: StrOption
Option that accept any textual data in Tiramisu:
```python
from tiramisu import StrOption
StrOption('str', 'str', 'value')
StrOption('str', 'str', '1')
```
Other type generate an error:
```python
try:
StrOption('str', 'str', 1)
except ValueError as err:
print(err)
```
returns:
```
"1" is an invalid string for "str"
```
## Integers option: IntOption
Option that accept any integers number in Tiramisu:
```python
from tiramisu import IntOption
IntOption('int', 'int', 1)
```
### min_number and max_number
This option can also verify minimal and maximal number:
```python
IntOption('int', 'int', 10, min_number=10, max_number=15)
```
```python
try:
IntOption('int', 'int', 16, max_number=15)
except ValueError as err:
print(err)
```
returns:
```
"16" is an invalid integer for "int", value must be less than "15"
```
> **_NOTE:_** If "warnings_only" parameter it set to True, it will only emit a warning.
## Floating point number option: FloatOption
Option that accept any floating point number in Tiramisu:
```python
from tiramisu import FloatOption
FloatOption('float', 'float', 10.1)
```
## Boolean option: BoolOption
Boolean values are the two constant objects False and True:
```python
from tiramisu import BoolOption
BoolOption('bool', 'bool', True)
BoolOption('bool', 'bool', False)
```
## Choice option: ChoiceOption
Option that only accepts a list of possible choices.
For example, we just want allowed 1 or 'see later':
```python
from tiramisu import ChoiceOption
ChoiceOption('choice', 'choice', (1, 'see later'), 1)
ChoiceOption('choice', 'choice', (1, 'see later'), 'see later')
```
Any other value isn't allowed:
```python
try:
ChoiceOption('choice', 'choice', (1, 'see later'), "i don't know")
except ValueError as err:
print(err)
```
returns:
```
"i don't know" is an invalid choice for "choice", only "1" and "see later" are allowed
```
# Network options
## IPv4 address option: IPOption
An Internet Protocol address (IP address) is a numerical label assigned to each device connected to a computer network that uses the Internet Protocol for communication.
This option only support version 4 of the Internet Protocol.
```python
from tiramisu import IPOption
IPOption('ip', 'ip', '192.168.0.24')
```
### private_only
By default IP could be a private or a public address. It's possible to restrict to only private IPv4 address with "private_only" attributs:
```python
try:
IPOption('ip', 'ip', '1.1.1.1', private_only=True)
except ValueError as err:
print(err)
```
returns:
```
"1.1.1.1" is an invalid IP for "ip", must be private IP
```
> **_NOTE:_** If "warnings_only" parameter it set to True, it will only emit a warning.
### allow_reserved
By default, IETF reserved are not allowed. The "allow_reserved" can be used to allow this address:
```python
try:
IPOption('ip', 'ip', '255.255.255.255')
except ValueError as err:
print(err)
```
returns:
```
"255.255.255.255" is an invalid IP for "ip", mustn't be reserved IP
```
But this doesn't raises:
```python
IPOption('ip', 'ip', '255.255.255.255', allow_reserved=True)
```
> **_NOTE:_** If "warnings_only" parameter it set to True, it will only emit a warning.
### cidr
Classless Inter-Domain Routing (CIDR) is a method for allocating IP addresses and IP routing.
CIDR notation, in which an address or routing prefix is written with a suffix indicating the number of bits of the prefix, such as 192.168.0.1/24.
```python
IPOption('ip', 'ip', '192.168.0.1/24', cidr=True)
try:
IPOption('ip', 'ip', '192.168.0.0/24', cidr=True)
except ValueError as err:
print(err)
```
returns:
```
"192.168.0.0/24" is an invalid IP for "ip", it's in fact a network address
```
## Port option: PortOption
A port is a network communication endpoint.
A port is a string object:
```python
from tiramisu import PortOption
PortOption('port', 'port', '80')
```
### allow_range
A range is a list of port where we specified first port and last port number. The separator is ":":
```python
PortOption('port', 'port', '2000', allow_range=True)
PortOption('port', 'port', '2000:3000', allow_range=True)
```
### allow_zero
By default, port 0 is not allowed, if you want allow it, use the parameter "allow_zero":
```python
try:
PortOption('port', 'port', '0')
except ValueError as err:
print(err)
```
returns:
```python
"0" is an invalid port for "port", must be between 1 and 49151
```
But this doesn't raises:
```python
PortOption('port', 'port', '0', allow_zero=True)
```
> **_NOTE:_** This option affect the minimal and maximal port number, if "warnings_only" parameter it set to True, it will only emit a warning.
### allow_wellknown
The well-known ports (also known as system ports) are those from 1 through 1023. This parameter is set to True by default:
```python
PortOption('port', 'port', '80')
```
```python
try:
PortOption('port', 'port', '80', allow_wellknown=False)
except ValueError as err:
print(err)
```
returns:
```
"80" is an invalid port for "port", must be between 1024 and 49151
```
> **_NOTE:_** This option affect the minimal and maximal port number, if "warnings_only" parameter it set to True, it will only emit a warning.
### allow_registred
The registered ports are those from 1024 through 49151. This parameter is set to True by default:
```python
PortOption('port', 'port', '1300')
```
```python
try:
PortOption('port', 'port', '1300', allow_registred=False)
except ValueError as err:
print(err)
```
returns:
```
"1300" is an invalid port for "port", must be between 1 and 1023
```
> **_NOTE:_** This option affect the minimal and maximal port number, if "warnings_only" parameter it set to True, it will only emit a warning.
### allow_protocol
```python
PortOption('port', 'port', '1300', allow_protocol="True")
PortOption('port', 'port', 'tcp:1300', allow_protocol="True")
PortOption('port', 'port', 'udp:1300', allow_protocol="True")
```
### allow_private
The dynamic or private ports are those from 49152 through 65535. One common use for this range is for ephemeral ports. This parameter is set to False by default:
```python
try:
PortOption('port', 'port', '64000')
except ValueError as err:
print(err)
```
returns:
```
"64000" is an invalid port for "port", must be between 1 and 49151
```
```python
PortOption('port', 'port', '64000', allow_private=True)
```
> **_NOTE:_** This option affect the minimal and maximal port number, if "warnings_only" parameter it set to True, it will only emit a warning.
## Subnetwork option: NetworkOption
IP networks may be divided into subnetworks:
```python
from tiramisu import NetworkOption
NetworkOption('net', 'net', '192.168.0.0')
```
### cidr
Classless Inter-Domain Routing (CIDR) is a method for allocating IP addresses and IP routing.
CIDR notation, in which an address or routing prefix is written with a suffix indicating the number of bits of the prefix, such as 192.168.0.0/24:
```python
NetworkOption('net', 'net', '192.168.0.0/24', cidr=True)
```
## Subnetwork mask option: NetmaskOption
For IPv4, a network may also be characterized by its subnet mask or netmask. This option allow you to enter a netmask:
```python
from tiramisu import NetmaskOption
NetmaskOption('mask', 'mask', '255.255.255.0')
```
## Broadcast option: BroadcastOption
The last address within a network broadcast transmission to all hosts on the link. This option allow you to enter a broadcast:
```python
from tiramisu import BroadcastOption
BroadcastOption('bcast', 'bcast', '192.168.0.254')
```
# Internet options
## Domain name option: DomainnameOption
Domain names are used in various networking contexts and for application-specific naming and addressing purposes.
The DomainnameOption allow different type of domain name:
```python
from tiramisu import DomainnameOption
DomainnameOption('domain', 'domain', 'foo.example.net')
DomainnameOption('domain', 'domain', 'foo', type='hostname')
```
### type
There is three type for a domain name:
- "domainname" (default): lowercase, number, "-" and "." characters are allowed, this must have at least one "."
- "hostname": lowercase, number and "-" characters are allowed, the maximum length is 63 characters
- "netbios": lowercase, number and "-" characters are allowed, the maximum length is 15 characters
> **_NOTE:_** If "warnings_only" parameter it set to True, it will raise if length is incorrect by only emit a warning character is not correct.
### allow_ip
If the option can contain a domain name or an IP, you can set the "allow_ip" to True, (False by default):
```python
DomainnameOption('domain', 'domain', 'foo.example.net', allow_ip=True)
DomainnameOption('domain', 'domain', '192.168.0.1', allow_ip=True)
```
In this case, IP is validate has IPOption would do.
### allow_cidr_network
If the option can contain a CIDR network, you can set "allow_cidr_network" to True, (False by default):
```python
DomainnameOption('domain', 'domain', 'foo.example.net', allow_cidr_network=True)
DomainnameOption('domain', 'domain', '192.168.0.0/24', allow_cidr_network=True)
```
### allow_without_dot
A domain name with domainname's type must have a dot. If "allow_without_dot" is True (False by default), we can set a domainname or an hostname:
```python
DomainnameOption('domain', 'domain', 'foo.example.net', allow_without_dot=True)
DomainnameOption('domain', 'domain', 'foo', allow_without_dot=True)
```
### allow_startswith_dot
A domain name with domainname's type mustn't start by a dot. .example.net is not a valid domain. In some case it could be interesting to allow domain name starts by a dot (for ACL in Squid, no proxy option in Firefox, ...):
```python
DomainnameOption('domain', 'domain', 'example.net', allow_startswith_dot=True)
DomainnameOption('domain', 'domain', '.example.net', allow_startswith_dot=True)
```
## MAC address option: MACOption
Validate MAC address:
```python
from tiramisu import MACOption
MACOption('mac', 'mac', '01:10:20:20:10:30')
```
## Uniform Resource Locator option: UrlOption
An Uniform Resource Locator is, in fact, a string starting with http:// or https://, a DomainnameOption, optionaly ':' and a PortOption, and finally filename:
```python
from tiramisu import URLOption
URLOption('url', 'url', 'http://foo.example.fr/index.php')
URLOption('url', 'url', 'https://foo.example.fr:4200/index.php?login=foo&pass=bar')
```
For parameters, see PortOption and DomainnameOption.
## Email address option: EmailOption
Electronic mail (email or e-mail) is a method of exchanging messages ("mail") between people using electronic devices. Here an example:
```python
from tiramisu import EmailOption
EmailOption('mail', 'mail', 'foo@example.net')
```
# Unix options
## Unix username: UsernameOption
An unix username option is a 32 characters maximum length with lowercase ASCII characters, number, '_' or '-'. The username have to start with lowercase ASCII characters or "_":
```python
from tiramisu import UsernameOption
UsernameOption('user', 'user', 'my_user')
```
## Unix groupname: GroupnameOption
Same condition for unix group name:
```python
from tiramisu import GroupnameOption
GroupnameOption('group', 'group', 'my_group')
```
## Password option: PasswordOption
Simple string with no other restriction:
```python
from tiramisu import PasswordOption
PasswordOption('pass', 'pass', 'oP$¨1jiJie')
```
## Unix filename option: FilenameOption
For this option, only lowercase and uppercas ASCII character, "-", ".", "_", "~", and "/" are allowed:
```python
from tiramisu import FilenameOption
FilenameOption('file', 'file', '/etc/tiramisu/tiramisu.conf')
```
# Date option
## Date option: DateOption
Date option waits for a date with format YYYY-MM-DD:
```python
from tiramisu import DateOption
DateOption('date', 'date', '2019-10-30')
```

183
doc/own_option.md Normal file
View file

@ -0,0 +1,183 @@
# Create it's own option
## Generic regexp option: "RegexpOption"
Use "RegexpOption" to create custom option is very simple.
You just have to create an object that inherits from "RegexpOption" and that has the following class attributes:
- \_\_slots\_\_: with new data members (the values should always be "tuple()")
- \_type = with a name
- \_display_name: with the display name (for example in error message)
- \_regexp: with a compiled regexp
Here an example to an option that only accept string with on lowercase ASCII vowel characters:
```python
import re
from tiramisu import RegexpOption
class VowelOption(RegexpOption):
__slots__ = tuple()
_type = 'vowel'
_display_name = "string with vowel"
_regexp = re.compile(r"^[aeiouy]*$")
```
Let's try our object:
```python
VowelOption('vowel', 'Vowel', 'aae')
```
```python
try:
VowelOption('vowel', 'Vowel', 'oooups')
except ValueError as err:
print(err)
```
returns:
```python
"oooups" is an invalid string with vowel for "Vowel"
```
## Create a custom option
An option always inherits from "Option" object. This object has the following class attributes:
- \_\_slots\_\_: a tuple with new data members (by default you should set "tuple()")
- \_type = with a name
- \_display_name: with the display name (for example in error message)
Here an example to an lipogram option:
```python
from tiramisu import Option
from tiramisu.error import ValueWarning
import warnings
class LipogramOption(Option):
__slots__ = tuple()
_type = 'lipogram'
_display_name = 'lipogram'
#
# First of all we want to add a custom parameter to ask the minimum length (min_len) of the value:
def __init__(self,
*args,
min_len=100,
**kwargs):
# store extra parameters
extra = {'_min_len': min_len}
super().__init__(*args,
extra=extra,
**kwargs)
#
# We have a first validation method.
# In this method, we verify that the value is a string and that there is no "e" on it:
# Even if user set warnings_only attribute, this method will raise.
def validate(self,
value):
# first, valid that the value is a string
if not isinstance(value, str):
raise ValueError('invalid string')
# and verify that there is any 'e' in the sentense
if 'e' in value:
raise ValueError('Perec wrote a book without any "e", you could not do it in a simple sentence?')
#
# Finally we add a method to valid the value length.
# If "warnings_only" is set to True, a warning will be emit:
def second_level_validation(self,
value,
warnings_only):
# retrive parameter in extra
min_len = self.impl_get_extra('_min_len')
# verify the sentense length
if len(value) < min_len:
# raise message, in this case, warning and error message are different
if warnings_only:
msg = f'it would be better to have at least {min_len} characters in the sentence'
else:
msg = f'you must have at least {min_len} characters in the sentence'
raise ValueError(msg)
```
Let's test it:
1. the character "e" is in the value:
```python
try:
LipogramOption('lipo',
'Lipogram',
'I just want to add a quality string that has no bad characters')
except ValueError as err:
print(err)
```
returns:
```
"I just want to add a quality string that has no bad characters" is an invalid lipogram for "Lipogram", Perec wrote a book without any "e", you could not do it in a simple sentence?
```
2. the character "e" is in the value and warnings_only is set to True:
```python
try:
LipogramOption('lipo',
'Lipogram',
'I just want to add a quality string that has no bad characters',
warnings_only=True)
except ValueError as err:
print(err)
```
```
"I just want to add a quality string that has no bad characters" is an invalid lipogram for "Lipogram", Perec wrote a book without any "e", you could not do it in a simple sentence?
```
3. the value is too short
```python
try:
LipogramOption('lipo',
'Lipogram',
'I just want to add a quality string that has no bad symbols')
except ValueError as err:
print(err)
```
returns:
```
"I just want to add a quality string that has no bad symbols" is an invalid lipogram for "Lipogram", you must have at least 100 characters in the sentence
```
4. the value is too short and warnings_only is set to True:
```python
warnings.simplefilter('always', ValueWarning)
with warnings.catch_warnings(record=True) as warn:
LipogramOption('lipo',
'Lipogram',
'I just want to add a quality string that has no bad symbols',
warnings_only=True)
if warn:
print(warn[0].message)
```
returns:
```
attention, "I just want to add a quality string that has no bad symbols" could be an invalid lipogram for "Lipogram", it would be better to have at least 100 characters in the sentence
```
5. set minimum length to 50 characters, the value is valid:
```python
LipogramOption('lipo',
'Lipogram',
'I just want to add a quality string that has no bad symbols',
min_len=50)
```

109
doc/property.md Normal file
View file

@ -0,0 +1,109 @@
# Property
## What are properties ?
The properties are a central element of Tiramisu.
Properties change the behavior of an option or make it unavailable.
## Read only and read write
Config can be in two defaut mode:
### read_only
You can get all variables in a "Config" that not disabled.
Off course, as the Config is read only, you cannot set any value to any option.
Only value with :doc`calculation` can change value.
You cannot access to mandatory variable without values. Verify that all values is set before change mode.
### read_write
You can get all options not disabled and not hidden. You can also set all variables not frozen.
## Common properties
### hidden
Option with this property can only get value in read only mode.
This property is used for option that user cannot modifify it's value (for example if it's value is calculated).
### disabled
We never can access to option with this property.
### frozen
Options with this property cannot be modified.
## Special option properties
### mandatory
You should set value for option with this properties. In read only mode we cannot access to this option if no value is set.
### empty or notempty
Only used for multi option that are not a follower.
Mandatory for a multi means that you cannot add None as a value. But value [] is allowed. This is not permit with "empty" property.
A multi option has automaticly "empty" property. If you don't want allow empty option, just add "notempty" property when you create the option.
### unique or notunique
Only used for multi option that are not a follower.
Raise ValueError if a value is set twice or more in a multi Option.
A multi option has automaticly "unique" property. If you want allow duplication in option, just add "notunique" property when you create the option.
### permissive
Option with 'permissive' cannot raise PropertiesOptionError for properties set in permissive.
Config with 'permissive', whole option in this config cannot raise PropertiesOptionError for properties set in permissive.
## Special Config properties
### cache
Enable cache settings and values.
### expire
Enable settings and values in cache expire after "expiration_time" (by default 5 seconds).
### everything_frozen
Whole options in config are frozen (even if option have not frozen property).
### force_default_on_freeze
Whole options frozen in config will lost his value and use default values.
### force_metaconfig_on_freeze
Whole options frozen in config will lost his value and get MetaConfig values.
### force_store_value
All options with this properties will be mark has modified.
### validator
Launch validator set by user in option (this property has no effect for option validation and second level validation).
### warnings
Display warnings during validation.
### demoting_error_warning
All value's errors are convert to warning (ValueErrorWarning).
## Own properties
There are no specific instructions for creating a property. Just add a string as property create a new property.

13
doc/symlinkoption.md Normal file
View file

@ -0,0 +1,13 @@
# The symbolic link option: SymLinkOption
A "SymLinkOption" is an option that actually points to another option.
Each time we will access to a properties of this options, we will have in return the value of other option.
Creation a "SymLinkOption" is easy:
```python
from tiramisu import StrOption, SymLinkOption
st = StrOption('str', 'str')
sym = SymLinkOption('sym', st)
```

494
doc/validator.md Normal file
View file

@ -0,0 +1,494 @@
# 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())
```

BIN
logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

140
logo.svg Normal file
View file

@ -0,0 +1,140 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="18.86796mm"
height="25.206835mm"
viewBox="0 0 18.867959 25.206835"
version="1.1"
id="svg5"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
sodipodi:docname="logo.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="4.404458"
inkscape:cx="1.3622562"
inkscape:cy="68.453372"
inkscape:window-width="1033"
inkscape:window-height="1080"
inkscape:window-x="26"
inkscape:window-y="23"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" />
<defs
id="defs2" />
<g
inkscape:label="Calque 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-78.220996,-39.872698)">
<text
xml:space="preserve"
style="font-size:2.52546px;line-height:1;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;stroke-width:0.631367"
x="93.453247"
y="48.8326"
id="text18049"><tspan
sodipodi:role="line"
id="tspan18047"
x="93.453247"
y="48.8326"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:2.52546px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:center;text-anchor:middle;stroke-width:0.631367">I</tspan><tspan
sodipodi:role="line"
x="93.453247"
y="51.358059"
id="tspan18051"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:2.52546px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:center;text-anchor:middle;stroke-width:0.631367">R</tspan><tspan
sodipodi:role="line"
x="93.453247"
y="53.883518"
id="tspan21745"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:2.52546px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:center;text-anchor:middle;stroke-width:0.631367">A</tspan><tspan
sodipodi:role="line"
x="93.453247"
y="56.408981"
id="tspan21747"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:2.52546px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:center;text-anchor:middle;stroke-width:0.631367">M</tspan><tspan
sodipodi:role="line"
x="93.453247"
y="58.934441"
id="tspan21749"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:2.52546px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:center;text-anchor:middle;stroke-width:0.631367">I</tspan><tspan
sodipodi:role="line"
x="93.453247"
y="61.4599"
id="tspan21751"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:2.52546px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:center;text-anchor:middle;stroke-width:0.631367">S</tspan><tspan
sodipodi:role="line"
x="93.453247"
y="63.985359"
id="tspan21753"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:2.52546px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:center;text-anchor:middle;stroke-width:0.631367">U</tspan></text>
<path
style="fill:#2a80af;fill-opacity:1;stroke:#000000;stroke-width:0.315683;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 81.95784,40.030698 13.710125,-1.58e-4 c 0.510538,0.02425 0.694831,0.0683 0.982497,0.311837 0.203261,0.250591 0.249844,0.413553 0.280237,0.871344 l -1.6e-5,2.550089 c 0,0 0.02416,0.6051 -0.27347,0.902731 -0.297626,0.297631 -0.46168,0.339646 -0.989248,0.360002 l -2.315853,1.55e-4 v -2.498 H 81.95784 Z"
id="path6247"
sodipodi:nodetypes="cccccsccccc" />
<ellipse
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.315695;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:7.7;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
id="path4757"
cx="95.089363"
cy="-42.528553"
transform="scale(1,-1)"
rx="0.61845386"
ry="0.61849999" />
<path
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0.315683;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 93.352109,45.026698 -13.710129,1.58e-4 c -0.51053,-0.02425 -0.69483,-0.0683 -0.98249,-0.311836 -0.20326,-0.250591 -0.24985,-0.413553 -0.28024,-0.871344 l 2e-5,-2.550089 c 0,0 -0.0242,-0.6051 0.27347,-0.902732 0.29762,-0.297631 0.46168,-0.339646 0.98924,-0.360002 l 2.31586,-1.55e-4 v 2.498 h 11.394269 z"
id="path6247-36"
sodipodi:nodetypes="cccccsccccc" />
<ellipse
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.315695;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:7.7;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
id="path4757-7"
cx="-80.220589"
cy="42.528843"
transform="scale(-1,1)"
rx="0.61845386"
ry="0.61849999" />
<g
id="g2099"
transform="rotate(-90,84.548611,28.232765)">
<path
style="fill:#2a80af;fill-opacity:1;stroke:#000000;stroke-width:0.315683;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 51.438689,29.084802 13.710125,-1.58e-4 c 0.510538,0.02425 0.694831,0.0683 0.982497,0.311837 0.203261,0.250591 0.249844,0.413553 0.280237,0.871344 l -1.6e-5,2.550089 c 0,0 0.02416,0.6051 -0.27347,0.902731 -0.297626,0.297631 -0.46168,0.339646 -0.989248,0.360002 l -2.315853,1.55e-4 v -2.498 H 51.438689 Z"
id="path6247-5"
sodipodi:nodetypes="cccccsccccc" />
<ellipse
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.315695;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:7.7;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
id="path4757-3"
cx="64.570213"
cy="-31.582657"
transform="scale(1,-1)"
rx="0.61845386"
ry="0.61849999" />
<path
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0.315683;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 62.832958,34.080802 -13.710129,1.58e-4 c -0.51053,-0.02425 -0.69483,-0.0683 -0.98249,-0.311836 -0.20326,-0.250591 -0.24985,-0.413553 -0.28024,-0.871344 l 2e-5,-2.550089 c 0,0 -0.0242,-0.6051 0.27347,-0.902732 0.29762,-0.297631 0.46168,-0.339646 0.98924,-0.360002 l 2.31586,-1.55e-4 v 2.498 h 11.394269 z"
id="path6247-36-5"
sodipodi:nodetypes="cccccsccccc" />
<ellipse
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.315695;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:7.7;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
id="path4757-7-6"
cx="-49.701439"
cy="31.582947"
transform="scale(-1,1)"
rx="0.61845386"
ry="0.61849999" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.8 KiB

View file

@ -14,7 +14,7 @@
# You should have received a copy of the GNU Lesser General Public License # You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# ____________________________________________________________ # ____________________________________________________________
from inspect import ismethod, getdoc, signature from inspect import ismethod, getdoc, signature, iscoroutinefunction
from time import time from time import time
from typing import List, Set, Any, Optional, Callable, Union, Dict from typing import List, Set, Any, Optional, Callable, Union, Dict
from warnings import catch_warnings, simplefilter from warnings import catch_warnings, simplefilter
@ -71,6 +71,9 @@ class TiramisuHelp:
display(_('Commands:')) display(_('Commands:'))
for module_name in modules: for module_name in modules:
module = getattr(self, module_name) module = getattr(self, module_name)
if not ('__getattr__' in dir(module) and iscoroutinefunction(module.__getattr__)) and \
hasattr(module, '__name__') and module.__name__ == 'wrapped':
module = module.func
doc = _(getdoc(module)) doc = _(getdoc(module))
display(self._tmpl_help.format(module_name, doc).expandtabs(max_len + 10)) display(self._tmpl_help.format(module_name, doc).expandtabs(max_len + 10))
display() display()
@ -231,6 +234,7 @@ def option_and_connection(func):
ret = await func(self, *args, **kwargs) ret = await func(self, *args, **kwargs)
del config_bag.connection del config_bag.connection
return ret return ret
wrapped.func = func
return wrapped return wrapped
@ -661,6 +665,7 @@ def option_type(typ):
ret = await func(*args, **kwargs) ret = await func(*args, **kwargs)
del config_bag.connection del config_bag.connection
return ret return ret
wrapped.func = func
return wrapped return wrapped
return wrapper return wrapper
@ -865,7 +870,7 @@ class TiramisuOption(CommonTiramisu, TiramisuConfig):
value=undefined, value=undefined,
type=None, type=None,
first: bool=False): first: bool=False):
"""find an option by name (only for optiondescription)""" """Find an option by name (only for optiondescription)"""
if not first: if not first:
ret = [] ret = []
option = self._option_bag.option option = self._option_bag.option
@ -967,7 +972,7 @@ class TiramisuOption(CommonTiramisu, TiramisuConfig):
remotable: str="minimum", remotable: str="minimum",
form: List=[], form: List=[],
force: bool=False) -> Dict: force: bool=False) -> Dict:
"""convert config and option to tiramisu format""" """Convert config and option to tiramisu format"""
if force or self._tiramisu_dict is None: if force or self._tiramisu_dict is None:
await self._load_dict(clearable, remotable) await self._load_dict(clearable, remotable)
return await self._tiramisu_dict.todict(form) return await self._tiramisu_dict.todict(form)
@ -975,7 +980,7 @@ class TiramisuOption(CommonTiramisu, TiramisuConfig):
@option_type('optiondescription') @option_type('optiondescription')
async def updates(self, async def updates(self,
body: List) -> Dict: body: List) -> Dict:
"""updates value with tiramisu format""" """Updates value with tiramisu format"""
if self._tiramisu_dict is None: if self._tiramisu_dict is None:
await self._load_dict() await self._load_dict()
return await self._tiramisu_dict.set_updates(body) return await self._tiramisu_dict.set_updates(body)
@ -989,6 +994,7 @@ def connection(func):
ret = await func(self, *args, **kwargs) ret = await func(self, *args, **kwargs)
del config_bag.connection del config_bag.connection
return ret return ret
wrapped.func = func
return wrapped return wrapped
@ -1447,14 +1453,14 @@ class TiramisuContextOption(TiramisuConfig, _TiramisuOptionWalk):
remotable="minimum", remotable="minimum",
form=[], form=[],
force=False): force=False):
"""convert config and option to tiramisu format""" """Convert config and option to tiramisu format"""
if force or self._tiramisu_dict is None: if force or self._tiramisu_dict is None:
await self._load_dict(clearable, remotable) await self._load_dict(clearable, remotable)
return await self._tiramisu_dict.todict(form) return await self._tiramisu_dict.todict(form)
async def updates(self, async def updates(self,
body: List) -> Dict: body: List) -> Dict:
"""updates value with tiramisu format""" """Updates value with tiramisu format"""
if self._tiramisu_dict is None: if self._tiramisu_dict is None:
await self._load_dict() await self._load_dict()
return await self._tiramisu_dict.set_updates(body) return await self._tiramisu_dict.set_updates(body)
@ -1565,7 +1571,7 @@ class _TiramisuContextGroupConfig(TiramisuConfig):
def __call__(self, def __call__(self,
path: Optional[str]): path: Optional[str]):
"""select a child Tiramisu config""" """Select a child Tiramisu config"""
spaths = path.split('.') spaths = path.split('.')
config = self._config_bag.context config = self._config_bag.context
for spath in spaths: for spath in spaths:
@ -1689,14 +1695,19 @@ class _TiramisuContextMetaConfig(_TiramisuContextMixConfig):
class TiramisuContextCache(TiramisuConfig): class TiramisuContextCache(TiramisuConfig):
"""Manage config cache"""
async def reset(self): async def reset(self):
"""Reset cache"""
await self._config_bag.context.cfgimpl_reset_cache(None, None) await self._config_bag.context.cfgimpl_reset_cache(None, None)
async def set_expiration_time(self, async def set_expiration_time(self,
time: int) -> None: time: int) -> None:
"""Change expiration time value"""
self._config_bag.expiration_time = time self._config_bag.expiration_time = time
async def get_expiration_time(self) -> int: async def get_expiration_time(self) -> int:
"""Get expiration time value"""
return self._config_bag.expiration_time return self._config_bag.expiration_time