Compare commits
10 commits
d736d09e42
...
477b0a2da1
| Author | SHA1 | Date | |
|---|---|---|---|
| 477b0a2da1 | |||
| 302be618ce | |||
| d2e790a2e2 | |||
| a16dbb60aa | |||
| ece7537b89 | |||
|
|
026e665ab0 | ||
|
|
9731769694 | ||
|
|
22ef5c7c3b | ||
|
|
89c095e0d3 | ||
|
|
7f78728ad1 |
33 changed files with 2976 additions and 125 deletions
|
|
@ -1,5 +1,9 @@
|
||||||
LICENSES
|

|
||||||
---------
|
|
||||||
|
[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
64
doc/README.md
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|

|
||||||
|
|
||||||
|
# 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
439
doc/api_value.md
Normal 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
350
doc/browse.md
Normal 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
107
doc/config.md
Normal 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 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
BIN
doc/config.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
57
doc/dynoptiondescription.md
Normal file
57
doc/dynoptiondescription.md
Normal 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
42
doc/gettingstarted.md
Normal 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
45
doc/leadership.md
Normal 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
134
doc/option.md
Normal 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
35
doc/optiondescription.md
Normal 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
485
doc/options.md
Normal 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
183
doc/own_option.md
Normal 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
109
doc/property.md
Normal 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
13
doc/symlinkoption.md
Normal 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
494
doc/validator.md
Normal 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
BIN
logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.6 KiB |
140
logo.svg
Normal file
140
logo.svg
Normal 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 |
|
|
@ -200,6 +200,29 @@ async def test_information_option():
|
||||||
assert not await list_sessions()
|
assert not await list_sessions()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_information_optiondescription():
|
||||||
|
descr = make_description()
|
||||||
|
async with await Config(descr) as cfg:
|
||||||
|
string = 'some informations'
|
||||||
|
#
|
||||||
|
assert list(await cfg.option('gc').information.list()) == ['doc']
|
||||||
|
await cfg.option('gc').information.set('info', string)
|
||||||
|
assert await cfg.option('gc').information.get('info') == string
|
||||||
|
assert set(await cfg.option('gc').information.list()) == {'doc', 'info'}
|
||||||
|
#
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
await cfg.option('gc').information.get('noinfo')
|
||||||
|
assert await cfg.option('gc').information.get('noinfo', 'default') == 'default'
|
||||||
|
await cfg.option('gc').information.reset('info')
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
await cfg.option('gc').information.get('info')
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
await cfg.option('gc').information.reset('noinfo')
|
||||||
|
assert list(await cfg.option('gc').information.list()) == ['doc']
|
||||||
|
assert not await list_sessions()
|
||||||
|
|
||||||
|
|
||||||
def compare(val1, val2):
|
def compare(val1, val2):
|
||||||
assert len(val1[0]) == len(val2[0])
|
assert len(val1[0]) == len(val2[0])
|
||||||
for idx1, val_1 in enumerate(val1[0]):
|
for idx1, val_1 in enumerate(val1[0]):
|
||||||
|
|
|
||||||
|
|
@ -371,6 +371,8 @@ async def test_invalid_option():
|
||||||
PortOption('a', '', allow_zero=True, allow_wellknown=False, allow_registred=True, allow_private=True)
|
PortOption('a', '', allow_zero=True, allow_wellknown=False, allow_registred=True, allow_private=True)
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
PortOption('a', '', allow_zero=False, allow_wellknown=False, allow_registred=False, allow_private=False)
|
PortOption('a', '', allow_zero=False, allow_wellknown=False, allow_registred=False, allow_private=False)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
PortOption('a', '', 'tcp:80')
|
||||||
NetworkOption('a', '')
|
NetworkOption('a', '')
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
NetworkOption('a', '', 'string')
|
NetworkOption('a', '', 'string')
|
||||||
|
|
|
||||||
|
|
@ -295,6 +295,16 @@ async def test_port(config_type):
|
||||||
assert not await list_sessions()
|
assert not await list_sessions()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_port_protocol(config_type):
|
||||||
|
a = PortOption('a', '', allow_protocol=True)
|
||||||
|
od = OptionDescription('od', '', [a])
|
||||||
|
async with await Config(od) as cfg:
|
||||||
|
await cfg.option('a').value.set('80')
|
||||||
|
await cfg.option('a').value.set('tcp:80')
|
||||||
|
assert not await list_sessions()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_port_range(config_type):
|
async def test_port_range(config_type):
|
||||||
a = PortOption('a', '', allow_range=True)
|
a = PortOption('a', '', allow_range=True)
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,8 @@ async def test_build_dyndescription():
|
||||||
od1 = OptionDescription('od', '', [dod])
|
od1 = OptionDescription('od', '', [dod])
|
||||||
async with await Config(od1) as cfg:
|
async with await Config(od1) as cfg:
|
||||||
assert await cfg.value.dict() == {'dodval1.stval1': None, 'dodval2.stval2': None}
|
assert await cfg.value.dict() == {'dodval1.stval1': None, 'dodval2.stval2': None}
|
||||||
|
assert await cfg.option('dodval1').option.isdynamic()
|
||||||
|
assert await cfg.option('dodval1.stval1').option.isdynamic()
|
||||||
assert not await list_sessions()
|
assert not await list_sessions()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -283,10 +283,17 @@ async def test_symlink_with_follower(config_type):
|
||||||
await cfg.option('ip_admin_eth0.ip_admin_eth0').value.set(['val1', 'val2'])
|
await cfg.option('ip_admin_eth0.ip_admin_eth0').value.set(['val1', 'val2'])
|
||||||
assert await cfg.value.dict() == {'ip_admin_eth0.ip_admin_eth0': ['val1', 'val2'], 'ip_admin_eth0.netmask_admin_eth0': [None, None], 'follower': [None, None]}
|
assert await cfg.value.dict() == {'ip_admin_eth0.ip_admin_eth0': ['val1', 'val2'], 'ip_admin_eth0.netmask_admin_eth0': [None, None], 'follower': [None, None]}
|
||||||
#
|
#
|
||||||
|
assert await cfg.option('ip_admin_eth0.netmask_admin_eth0', 0).owner.get() == 'default'
|
||||||
|
assert await cfg.option('ip_admin_eth0.netmask_admin_eth0', 1).owner.get() == 'default'
|
||||||
|
assert await cfg.option('follower', 0).owner.get() == 'default'
|
||||||
|
assert await cfg.option('follower', 1).owner.get() == 'default'
|
||||||
|
assert await cfg.option('follower').owner.get() == ['default', 'default']
|
||||||
|
#
|
||||||
assert await cfg.option('ip_admin_eth0.netmask_admin_eth0', 0).value.get() == None
|
assert await cfg.option('ip_admin_eth0.netmask_admin_eth0', 0).value.get() == None
|
||||||
assert await cfg.option('ip_admin_eth0.netmask_admin_eth0', 1).value.get() == None
|
assert await cfg.option('ip_admin_eth0.netmask_admin_eth0', 1).value.get() == None
|
||||||
assert await cfg.option('follower', 0).value.get() == None
|
assert await cfg.option('follower', 0).value.get() == None
|
||||||
assert await cfg.option('follower', 1).value.get() == None
|
assert await cfg.option('follower', 1).value.get() == None
|
||||||
|
assert await cfg.option('follower').value.get() == [None, None]
|
||||||
#
|
#
|
||||||
await cfg.option('ip_admin_eth0.netmask_admin_eth0', 1).value.set('val3')
|
await cfg.option('ip_admin_eth0.netmask_admin_eth0', 1).value.set('val3')
|
||||||
assert await cfg.value.dict() == {'ip_admin_eth0.ip_admin_eth0': ['val1', 'val2'], 'ip_admin_eth0.netmask_admin_eth0': [None, 'val3'], 'follower': [None, 'val3']}
|
assert await cfg.value.dict() == {'ip_admin_eth0.ip_admin_eth0': ['val1', 'val2'], 'ip_admin_eth0.netmask_admin_eth0': [None, 'val3'], 'follower': [None, 'val3']}
|
||||||
|
|
@ -295,6 +302,13 @@ async def test_symlink_with_follower(config_type):
|
||||||
assert await cfg.option('ip_admin_eth0.netmask_admin_eth0', 1).value.get() == 'val3'
|
assert await cfg.option('ip_admin_eth0.netmask_admin_eth0', 1).value.get() == 'val3'
|
||||||
assert await cfg.option('follower', 0).value.get() == None
|
assert await cfg.option('follower', 0).value.get() == None
|
||||||
assert await cfg.option('follower', 1).value.get() == 'val3'
|
assert await cfg.option('follower', 1).value.get() == 'val3'
|
||||||
|
assert await cfg.option('follower').value.get() == [None, 'val3']
|
||||||
|
#
|
||||||
|
assert await cfg.option('ip_admin_eth0.netmask_admin_eth0', 0).owner.get() == 'default'
|
||||||
|
assert await cfg.option('ip_admin_eth0.netmask_admin_eth0', 1).owner.get() == 'user'
|
||||||
|
assert await cfg.option('follower', 0).owner.get() == 'default'
|
||||||
|
assert await cfg.option('follower', 1).owner.get() == 'user'
|
||||||
|
assert await cfg.option('follower').owner.get() == ['default', 'user']
|
||||||
assert not await list_sessions()
|
assert not await list_sessions()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
256
tiramisu/api.py
256
tiramisu/api.py
|
|
@ -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()
|
||||||
|
|
@ -142,9 +145,88 @@ class CommonTiramisuOption(CommonTiramisu):
|
||||||
self._subconfig = None
|
self._subconfig = None
|
||||||
|
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
|
if name == '__name__':
|
||||||
|
return self.__class__.__name__
|
||||||
raise APIError(_('unknown method "{}" in "{}"').format(name, self.__class__.__name__))
|
raise APIError(_('unknown method "{}" in "{}"').format(name, self.__class__.__name__))
|
||||||
|
|
||||||
|
|
||||||
|
class _TiramisuOptionWalk:
|
||||||
|
async def _filter(self,
|
||||||
|
opt,
|
||||||
|
subconfig,
|
||||||
|
config_bag,
|
||||||
|
):
|
||||||
|
option_bag = OptionBag()
|
||||||
|
option_bag.set_option(opt,
|
||||||
|
None,
|
||||||
|
config_bag)
|
||||||
|
settings = config_bag.context.cfgimpl_get_settings()
|
||||||
|
option_bag.properties = await settings.getproperties(option_bag)
|
||||||
|
if opt.impl_is_optiondescription():
|
||||||
|
await settings.validate_properties(option_bag)
|
||||||
|
return await subconfig.get_subconfig(option_bag)
|
||||||
|
await subconfig.getattr(opt.impl_getname(),
|
||||||
|
option_bag)
|
||||||
|
|
||||||
|
async def _walk(self,
|
||||||
|
option,
|
||||||
|
recursive,
|
||||||
|
type_,
|
||||||
|
group_type,
|
||||||
|
config_bag,
|
||||||
|
subconfig,
|
||||||
|
):
|
||||||
|
options = []
|
||||||
|
for opt in await option.get_children(config_bag):
|
||||||
|
try:
|
||||||
|
subsubconfig = await self._filter(opt,
|
||||||
|
subconfig,
|
||||||
|
config_bag)
|
||||||
|
except PropertiesOptionError:
|
||||||
|
continue
|
||||||
|
if opt.impl_is_optiondescription():
|
||||||
|
if recursive:
|
||||||
|
options.extend(await self._walk(opt,
|
||||||
|
recursive,
|
||||||
|
type_,
|
||||||
|
group_type,
|
||||||
|
config_bag,
|
||||||
|
subsubconfig))
|
||||||
|
if type_ == 'option' or (type_ == 'optiondescription' and \
|
||||||
|
group_type and opt.impl_get_group_type() != group_type):
|
||||||
|
continue
|
||||||
|
elif type_ == 'optiondescription':
|
||||||
|
continue
|
||||||
|
options.append(TiramisuOption(opt.impl_getpath(),
|
||||||
|
None,
|
||||||
|
config_bag,
|
||||||
|
))
|
||||||
|
return options
|
||||||
|
|
||||||
|
async def _list(self,
|
||||||
|
type,
|
||||||
|
group_type,
|
||||||
|
recursive,
|
||||||
|
root_option,
|
||||||
|
config_bag,
|
||||||
|
):
|
||||||
|
assert type in ('all', 'option', 'optiondescription'), _('unknown list type {}').format(type)
|
||||||
|
assert group_type is None or isinstance(group_type, groups.GroupType), \
|
||||||
|
_("unknown group_type: {0}").format(group_type)
|
||||||
|
if config_bag.properties and 'warnings' in config_bag.properties:
|
||||||
|
config_bag = config_bag.copy()
|
||||||
|
config_bag.remove_warnings()
|
||||||
|
options = []
|
||||||
|
for opt in await self._walk(root_option,
|
||||||
|
recursive,
|
||||||
|
type,
|
||||||
|
group_type,
|
||||||
|
config_bag,
|
||||||
|
config_bag.context):
|
||||||
|
options.append(opt)
|
||||||
|
return options
|
||||||
|
|
||||||
|
|
||||||
def option_and_connection(func):
|
def option_and_connection(func):
|
||||||
async def wrapped(self, *args, **kwargs):
|
async def wrapped(self, *args, **kwargs):
|
||||||
config_bag = self._option_bag.config_bag
|
config_bag = self._option_bag.config_bag
|
||||||
|
|
@ -154,10 +236,11 @@ 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
|
||||||
|
|
||||||
|
|
||||||
class _TiramisuOptionOptionDescription(CommonTiramisuOption):
|
class _TiramisuOptionOptionDescription(CommonTiramisuOption, _TiramisuOptionWalk):
|
||||||
"""Manage option"""
|
"""Manage option"""
|
||||||
_allow_optiondescription = True
|
_allow_optiondescription = True
|
||||||
_follower_need_index = False
|
_follower_need_index = False
|
||||||
|
|
@ -182,6 +265,11 @@ class _TiramisuOptionOptionDescription(CommonTiramisuOption):
|
||||||
"""Test if option is a leader or a follower"""
|
"""Test if option is a leader or a follower"""
|
||||||
return self._option_bag.option.impl_is_leadership()
|
return self._option_bag.option.impl_is_leadership()
|
||||||
|
|
||||||
|
@option_and_connection
|
||||||
|
async def isdynamic(self):
|
||||||
|
"""Test if option is a dynamic optiondescription"""
|
||||||
|
return self._option_bag.option.impl_is_dynoptiondescription()
|
||||||
|
|
||||||
@option_and_connection
|
@option_and_connection
|
||||||
async def doc(self):
|
async def doc(self):
|
||||||
"""Get option document"""
|
"""Get option document"""
|
||||||
|
|
@ -278,6 +366,11 @@ class TiramisuOptionOption(_TiramisuOptionOptionDescription):
|
||||||
"""Test if option is a follower"""
|
"""Test if option is a follower"""
|
||||||
return self._option_bag.option.impl_is_follower()
|
return self._option_bag.option.impl_is_follower()
|
||||||
|
|
||||||
|
@option_and_connection
|
||||||
|
async def isdynamic(self):
|
||||||
|
"""Test if option is a dynamic optiondescription"""
|
||||||
|
return self._option_bag.option.impl_is_dynsymlinkoption()
|
||||||
|
|
||||||
@option_and_connection
|
@option_and_connection
|
||||||
async def issymlinkoption(self) -> bool:
|
async def issymlinkoption(self) -> bool:
|
||||||
return self._option_bag.option.impl_is_symlinkoption()
|
return self._option_bag.option.impl_is_symlinkoption()
|
||||||
|
|
@ -331,6 +424,19 @@ class TiramisuOptionOption(_TiramisuOptionOptionDescription):
|
||||||
#FIXME only from 0.0.0.0 to 255.255.255.255
|
#FIXME only from 0.0.0.0 to 255.255.255.255
|
||||||
return r'^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$'
|
return r'^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$'
|
||||||
|
|
||||||
|
@option_and_connection
|
||||||
|
async def list(self,
|
||||||
|
type='option',
|
||||||
|
group_type=None,
|
||||||
|
recursive=False,
|
||||||
|
):
|
||||||
|
return await self._list(type,
|
||||||
|
group_type,
|
||||||
|
recursive,
|
||||||
|
self._option_bag.option,
|
||||||
|
self._option_bag.config_bag,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TiramisuOptionOwner(CommonTiramisuOption):
|
class TiramisuOptionOwner(CommonTiramisuOption):
|
||||||
#FIXME optiondescription must not have Owner!
|
#FIXME optiondescription must not have Owner!
|
||||||
|
|
@ -347,6 +453,19 @@ class TiramisuOptionOwner(CommonTiramisuOption):
|
||||||
@option_and_connection
|
@option_and_connection
|
||||||
async def get(self):
|
async def get(self):
|
||||||
"""Get owner for a specified option"""
|
"""Get owner for a specified option"""
|
||||||
|
if self._option_bag.option.impl_is_follower() and self._option_bag.index is None:
|
||||||
|
if self._option_bag.option.impl_is_symlinkoption():
|
||||||
|
length = await self._subconfig.cfgimpl_get_length_leadership(self._option_bag)
|
||||||
|
value = []
|
||||||
|
for index in range(length):
|
||||||
|
soption_bag = self._option_bag.copy()
|
||||||
|
soption_bag.index = index
|
||||||
|
value.append(await self._values.getowner(soption_bag,
|
||||||
|
self._name,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
raise APIError('index must be set with a follower option')
|
||||||
return await self._values.getowner(self._option_bag)
|
return await self._values.getowner(self._option_bag)
|
||||||
|
|
||||||
@option_and_connection
|
@option_and_connection
|
||||||
|
|
@ -559,8 +678,12 @@ def option_type(typ):
|
||||||
del config_bag.connection
|
del config_bag.connection
|
||||||
raise APIError(_('please specify a valid sub function ({})').format(func.__name__))
|
raise APIError(_('please specify a valid sub function ({})').format(func.__name__))
|
||||||
ret = await func(*args, **kwargs)
|
ret = await func(*args, **kwargs)
|
||||||
del config_bag.connection
|
try:
|
||||||
|
del config_bag.connection
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
return ret
|
return ret
|
||||||
|
wrapped.func = func
|
||||||
return wrapped
|
return wrapped
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
@ -593,6 +716,17 @@ class TiramisuOptionValue(CommonTiramisuOption):
|
||||||
async def get(self):
|
async def get(self):
|
||||||
"""Get option's value"""
|
"""Get option's value"""
|
||||||
if self._option_bag.option.impl_is_follower() and self._option_bag.index is None:
|
if self._option_bag.option.impl_is_follower() and self._option_bag.index is None:
|
||||||
|
# if it's a follower included in a symlinkoption, this should consider as a list
|
||||||
|
if self._option_bag.option.impl_is_symlinkoption():
|
||||||
|
value = []
|
||||||
|
for index in range(await self._len()):
|
||||||
|
soption_bag = self._option_bag.copy()
|
||||||
|
soption_bag.index = index
|
||||||
|
value.append(await self._subconfig.getattr(self._name,
|
||||||
|
soption_bag,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return value
|
||||||
raise APIError('index must be set with a follower option')
|
raise APIError('index must be set with a follower option')
|
||||||
return await self._subconfig.getattr(self._name,
|
return await self._subconfig.getattr(self._name,
|
||||||
self._option_bag,
|
self._option_bag,
|
||||||
|
|
@ -679,6 +813,9 @@ class TiramisuOptionValue(CommonTiramisuOption):
|
||||||
|
|
||||||
@option_type('follower')
|
@option_type('follower')
|
||||||
async def len(self):
|
async def len(self):
|
||||||
|
return await self._len()
|
||||||
|
|
||||||
|
async def _len(self):
|
||||||
"""Length of follower option"""
|
"""Length of follower option"""
|
||||||
# for example if index is None
|
# for example if index is None
|
||||||
if '_length' not in vars(self):
|
if '_length' not in vars(self):
|
||||||
|
|
@ -765,7 +902,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
|
||||||
|
|
@ -798,7 +935,8 @@ class TiramisuOption(CommonTiramisu, TiramisuConfig):
|
||||||
async def _filter(self,
|
async def _filter(self,
|
||||||
opt,
|
opt,
|
||||||
subconfig,
|
subconfig,
|
||||||
config_bag):
|
config_bag,
|
||||||
|
):
|
||||||
settings = config_bag.context.cfgimpl_get_settings()
|
settings = config_bag.context.cfgimpl_get_settings()
|
||||||
option_bag = OptionBag()
|
option_bag = OptionBag()
|
||||||
option_bag.set_option(opt,
|
option_bag.set_option(opt,
|
||||||
|
|
@ -834,7 +972,8 @@ class TiramisuOption(CommonTiramisu, TiramisuConfig):
|
||||||
try:
|
try:
|
||||||
await self._filter(opt,
|
await self._filter(opt,
|
||||||
subconfig,
|
subconfig,
|
||||||
config_bag)
|
config_bag,
|
||||||
|
)
|
||||||
except PropertiesOptionError:
|
except PropertiesOptionError:
|
||||||
continue
|
continue
|
||||||
if opt.impl_is_optiondescription():
|
if opt.impl_is_optiondescription():
|
||||||
|
|
@ -865,7 +1004,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)
|
||||||
|
|
@ -873,7 +1012,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)
|
||||||
|
|
@ -887,6 +1026,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
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1289,7 +1429,7 @@ class TiramisuContextPermissive(TiramisuConfig):
|
||||||
await self._set(frozenset(props))
|
await self._set(frozenset(props))
|
||||||
|
|
||||||
|
|
||||||
class TiramisuContextOption(TiramisuConfig):
|
class TiramisuContextOption(TiramisuConfig, _TiramisuOptionWalk):
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
*args,
|
*args,
|
||||||
**kwargs) -> None:
|
**kwargs) -> None:
|
||||||
|
|
@ -1317,78 +1457,19 @@ class TiramisuContextOption(TiramisuConfig):
|
||||||
options.append(option)
|
options.append(option)
|
||||||
return options
|
return options
|
||||||
|
|
||||||
async def _filter(self,
|
|
||||||
opt,
|
|
||||||
subconfig,
|
|
||||||
config_bag):
|
|
||||||
option_bag = OptionBag()
|
|
||||||
option_bag.set_option(opt,
|
|
||||||
None,
|
|
||||||
config_bag)
|
|
||||||
settings = config_bag.context.cfgimpl_get_settings()
|
|
||||||
option_bag.properties = await settings.getproperties(option_bag)
|
|
||||||
if opt.impl_is_optiondescription():
|
|
||||||
await settings.validate_properties(option_bag)
|
|
||||||
return await subconfig.get_subconfig(option_bag)
|
|
||||||
await subconfig.getattr(opt.impl_getname(),
|
|
||||||
option_bag)
|
|
||||||
|
|
||||||
async def _walk(self,
|
|
||||||
option,
|
|
||||||
recursive,
|
|
||||||
type_,
|
|
||||||
group_type,
|
|
||||||
config_bag,
|
|
||||||
subconfig):
|
|
||||||
options = []
|
|
||||||
for opt in await option.get_children(config_bag):
|
|
||||||
try:
|
|
||||||
subsubconfig = await self._filter(opt,
|
|
||||||
subconfig,
|
|
||||||
config_bag)
|
|
||||||
except PropertiesOptionError:
|
|
||||||
continue
|
|
||||||
if opt.impl_is_optiondescription():
|
|
||||||
if recursive:
|
|
||||||
options.extend(await self._walk(opt,
|
|
||||||
recursive,
|
|
||||||
type_,
|
|
||||||
group_type,
|
|
||||||
config_bag,
|
|
||||||
subsubconfig))
|
|
||||||
if type_ == 'option' or (type_ == 'optiondescription' and \
|
|
||||||
group_type and opt.impl_get_group_type() != group_type):
|
|
||||||
continue
|
|
||||||
elif type_ == 'optiondescription':
|
|
||||||
continue
|
|
||||||
options.append(TiramisuOption(opt.impl_getpath(),
|
|
||||||
None,
|
|
||||||
self._config_bag))
|
|
||||||
return options
|
|
||||||
|
|
||||||
@connection
|
@connection
|
||||||
async def list(self,
|
async def list(self,
|
||||||
type='option',
|
type='option',
|
||||||
group_type=None,
|
group_type=None,
|
||||||
recursive=False):
|
recursive=False,
|
||||||
|
):
|
||||||
"""List options (by default list only option)"""
|
"""List options (by default list only option)"""
|
||||||
assert type in ('all', 'option', 'optiondescription'), _('unknown list type {}').format(type)
|
return await self._list(type,
|
||||||
assert group_type is None or isinstance(group_type, groups.GroupType), \
|
group_type,
|
||||||
_("unknown group_type: {0}").format(group_type)
|
recursive,
|
||||||
config_bag = self._config_bag
|
self._config_bag.context.cfgimpl_get_description(),
|
||||||
if config_bag.properties and 'warnings' in config_bag.properties:
|
self._config_bag,
|
||||||
config_bag = config_bag.copy()
|
)
|
||||||
config_bag.remove_warnings()
|
|
||||||
option = config_bag.context.cfgimpl_get_description()
|
|
||||||
options = []
|
|
||||||
for opt in await self._walk(option,
|
|
||||||
recursive,
|
|
||||||
type,
|
|
||||||
group_type,
|
|
||||||
config_bag,
|
|
||||||
config_bag.context):
|
|
||||||
options.append(opt)
|
|
||||||
return options
|
|
||||||
|
|
||||||
async def _load_dict(self,
|
async def _load_dict(self,
|
||||||
clearable="all",
|
clearable="all",
|
||||||
|
|
@ -1404,14 +1485,14 @@ class TiramisuContextOption(TiramisuConfig):
|
||||||
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)
|
||||||
|
|
@ -1522,7 +1603,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:
|
||||||
|
|
@ -1646,14 +1727,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
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1740,7 +1826,8 @@ class Config(TiramisuAPI):
|
||||||
session_id: str=None,
|
session_id: str=None,
|
||||||
delete_old_session: bool=False,
|
delete_old_session: bool=False,
|
||||||
storage=None,
|
storage=None,
|
||||||
display_name=None) -> None:
|
display_name=None,
|
||||||
|
) -> None:
|
||||||
if storage is None:
|
if storage is None:
|
||||||
storage = default_storage
|
storage = default_storage
|
||||||
storage_obj = await storage.get()
|
storage_obj = await storage.get()
|
||||||
|
|
@ -1753,14 +1840,17 @@ class Config(TiramisuAPI):
|
||||||
session_id=session_id,
|
session_id=session_id,
|
||||||
delete_old_session=delete_old_session,
|
delete_old_session=delete_old_session,
|
||||||
storage=storage,
|
storage=storage,
|
||||||
display_name=display_name)
|
display_name=display_name,
|
||||||
|
)
|
||||||
settings = config.cfgimpl_get_settings()
|
settings = config.cfgimpl_get_settings()
|
||||||
properties = await settings.get_context_properties(connection,
|
properties = await settings.get_context_properties(connection,
|
||||||
config._impl_properties_cache)
|
config._impl_properties_cache,
|
||||||
|
)
|
||||||
permissives = await settings.get_context_permissives(connection)
|
permissives = await settings.get_context_permissives(connection)
|
||||||
config_bag = ConfigBag(config,
|
config_bag = ConfigBag(config,
|
||||||
properties=properties,
|
properties=properties,
|
||||||
permissives=permissives)
|
permissives=permissives,
|
||||||
|
)
|
||||||
super().__init__(config_bag)
|
super().__init__(config_bag)
|
||||||
|
|
||||||
async def __aenter__(self):
|
async def __aenter__(self):
|
||||||
|
|
|
||||||
|
|
@ -279,9 +279,7 @@ async def manager_callback(callbk: Param,
|
||||||
except ValueError as err:
|
except ValueError as err:
|
||||||
raise ValueError(_('the option "{0}" is used in a calculation but is invalid ({1})').format(option_bag.option.impl_get_display_name(), err))
|
raise ValueError(_('the option "{0}" is used in a calculation but is invalid ({1})').format(option_bag.option.impl_get_display_name(), err))
|
||||||
except AttributeError as err:
|
except AttributeError as err:
|
||||||
raise ConfigError(_('impossible to calculate "{0}", {1}').format(option_bag.option.impl_get_display_name(),
|
raise ConfigError(_(f'unable to get value for calculating "{option_bag.option.impl_get_display_name()}", {err}'))
|
||||||
err,
|
|
||||||
))
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
async def get_option_bag(config_bag,
|
async def get_option_bag(config_bag,
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,6 @@ class SubConfig:
|
||||||
desc,
|
desc,
|
||||||
resetted_opts,
|
resetted_opts,
|
||||||
option_bag):
|
option_bag):
|
||||||
|
|
||||||
if option_bag.path in resetted_opts:
|
if option_bag.path in resetted_opts:
|
||||||
return
|
return
|
||||||
resetted_opts.append(option_bag.path)
|
resetted_opts.append(option_bag.path)
|
||||||
|
|
@ -185,16 +184,13 @@ class SubConfig:
|
||||||
context = self.cfgimpl_get_context()
|
context = self.cfgimpl_get_context()
|
||||||
desc = context.cfgimpl_get_description()
|
desc = context.cfgimpl_get_description()
|
||||||
if option_bag is not None:
|
if option_bag is not None:
|
||||||
if 'cache' in option_bag.config_bag.properties:
|
if 'cache' not in option_bag.config_bag.properties:
|
||||||
has_cache = True
|
return
|
||||||
option_bag.config_bag.properties = option_bag.config_bag.properties - {'cache'}
|
option_bag.config_bag.properties = option_bag.config_bag.properties - {'cache'}
|
||||||
else:
|
|
||||||
has_cache = False
|
|
||||||
await self.reset_one_option_cache(desc,
|
await self.reset_one_option_cache(desc,
|
||||||
resetted_opts,
|
resetted_opts,
|
||||||
option_bag)
|
option_bag)
|
||||||
if has_cache:
|
option_bag.config_bag.properties = option_bag.config_bag.properties | {'cache'}
|
||||||
option_bag.config_bag.properties = option_bag.config_bag.properties | {'cache'}
|
|
||||||
else:
|
else:
|
||||||
context._impl_values_cache.reset_all_cache()
|
context._impl_values_cache.reset_all_cache()
|
||||||
context._impl_properties_cache.reset_all_cache()
|
context._impl_properties_cache.reset_all_cache()
|
||||||
|
|
|
||||||
|
|
@ -45,8 +45,8 @@ def get_translation() -> str:
|
||||||
from locale import windows_locale
|
from locale import windows_locale
|
||||||
default_locale = windows_locale[ctypes.windll.kernel32.GetUserDefaultUILanguage()]
|
default_locale = windows_locale[ctypes.windll.kernel32.GetUserDefaultUILanguage()]
|
||||||
else:
|
else:
|
||||||
from locale import getdefaultlocale
|
from locale import getlocale
|
||||||
default_locale = getdefaultlocale()
|
default_locale = getlocale()
|
||||||
if default_locale and isinstance(default_locale, tuple):
|
if default_locale and isinstance(default_locale, tuple):
|
||||||
if default_locale[0] is not None:
|
if default_locale[0] is not None:
|
||||||
user_locale = default_locale[0][:2]
|
user_locale = default_locale[0][:2]
|
||||||
|
|
|
||||||
|
|
@ -221,8 +221,7 @@ class Base:
|
||||||
return dico[key]
|
return dico[key]
|
||||||
if default is not undefined:
|
if default is not undefined:
|
||||||
return default
|
return default
|
||||||
raise ValueError(_("information's item not found: {0}").format(
|
raise ValueError(_(f'information\'s item for "{self.impl_get_display_name()}" not found: "{key}"'))
|
||||||
key))
|
|
||||||
|
|
||||||
def impl_set_information(self,
|
def impl_set_information(self,
|
||||||
key: str,
|
key: str,
|
||||||
|
|
@ -334,3 +333,12 @@ class BaseOption(Base):
|
||||||
|
|
||||||
def impl_is_symlinkoption(self) -> bool:
|
def impl_is_symlinkoption(self) -> bool:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def get_dependencies_information(self,
|
||||||
|
itself=False,
|
||||||
|
) -> List[str]:
|
||||||
|
if itself:
|
||||||
|
idx = 1
|
||||||
|
else:
|
||||||
|
idx = 0
|
||||||
|
return getattr(self, '_dependencies_information', [[], []])[idx]
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ class IPOption(StrOption):
|
||||||
try:
|
try:
|
||||||
new_value = str(ip_address(value))
|
new_value = str(ip_address(value))
|
||||||
if value != new_value:
|
if value != new_value:
|
||||||
raise ValueError(f'should be {new_value}')
|
raise ValueError(_(f'should be {new_value}'))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise ValueError()
|
raise ValueError()
|
||||||
|
|
||||||
|
|
@ -72,6 +72,8 @@ class IPOption(StrOption):
|
||||||
value: str) -> None:
|
value: str) -> None:
|
||||||
super().validate(value)
|
super().validate(value)
|
||||||
if self.impl_get_extra('_cidr'):
|
if self.impl_get_extra('_cidr'):
|
||||||
|
if '/' not in value:
|
||||||
|
raise ValueError(_('CIDR address must have a "/"'))
|
||||||
self._validate_cidr(value)
|
self._validate_cidr(value)
|
||||||
else:
|
else:
|
||||||
self._validate_ip(value)
|
self._validate_ip(value)
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ from .baseoption import BaseOption, submulti, STATIC_TUPLE
|
||||||
from ..i18n import _
|
from ..i18n import _
|
||||||
from ..setting import undefined, OptionBag, Undefined
|
from ..setting import undefined, OptionBag, Undefined
|
||||||
from ..autolib import Calculation, Params, ParamOption, ParamInformation, ParamSelfInformation
|
from ..autolib import Calculation, Params, ParamOption, ParamInformation, ParamSelfInformation
|
||||||
from ..error import (ConfigError, ValueWarning, ValueErrorWarning, PropertiesOptionError,
|
from ..error import (ConfigError, ValueWarning, ValueErrorWarning,
|
||||||
ValueOptionError, display_list)
|
ValueOptionError, display_list)
|
||||||
from .syndynoption import SynDynOption
|
from .syndynoption import SynDynOption
|
||||||
#ALLOWED_CONST_LIST = ['_cons_not_equal']
|
#ALLOWED_CONST_LIST = ['_cons_not_equal']
|
||||||
|
|
@ -207,15 +207,6 @@ class Option(BaseOption):
|
||||||
def impl_is_dynsymlinkoption(self) -> bool:
|
def impl_is_dynsymlinkoption(self) -> bool:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_dependencies_information(self,
|
|
||||||
itself=False,
|
|
||||||
) -> List[str]:
|
|
||||||
if itself:
|
|
||||||
idx = 1
|
|
||||||
else:
|
|
||||||
idx = 0
|
|
||||||
return getattr(self, '_dependencies_information', [[], []])[idx]
|
|
||||||
|
|
||||||
def get_type(self) -> str:
|
def get_type(self) -> str:
|
||||||
# _display_name for compatibility with older version than 3.0rc3
|
# _display_name for compatibility with older version than 3.0rc3
|
||||||
return getattr(self, '_type', self._display_name)
|
return getattr(self, '_type', self._display_name)
|
||||||
|
|
@ -347,14 +338,17 @@ class Option(BaseOption):
|
||||||
|
|
||||||
def _is_not_unique(value, option_bag):
|
def _is_not_unique(value, option_bag):
|
||||||
# if set(value) has not same length than value
|
# if set(value) has not same length than value
|
||||||
if config_bag is not undefined and check_error and \
|
if config_bag is undefined or not check_error or \
|
||||||
'unique' in option_bag.properties:
|
'unique' not in option_bag.properties:
|
||||||
lvalue = [val for val in value if val is not None]
|
return
|
||||||
if len(set(lvalue)) != len(lvalue):
|
lvalue = [val for val in value if val is not None]
|
||||||
for idx, val in enumerate(value):
|
if len(set(lvalue)) == len(lvalue):
|
||||||
if val in value[idx+1:]:
|
return
|
||||||
raise ValueError(_('the value "{}" is not unique'
|
for idx, val in enumerate(value):
|
||||||
'').format(val))
|
if val not in value[idx+1:]:
|
||||||
|
continue
|
||||||
|
raise ValueError(_('the value "{}" is not unique'
|
||||||
|
'').format(val))
|
||||||
|
|
||||||
async def calculation_validator(val,
|
async def calculation_validator(val,
|
||||||
_index):
|
_index):
|
||||||
|
|
|
||||||
|
|
@ -48,12 +48,15 @@ class PortOption(StrOption):
|
||||||
allow_zero: bool=False,
|
allow_zero: bool=False,
|
||||||
allow_wellknown: bool=True,
|
allow_wellknown: bool=True,
|
||||||
allow_registred: bool=True,
|
allow_registred: bool=True,
|
||||||
|
allow_protocol: bool=False,
|
||||||
allow_private: bool=False,
|
allow_private: bool=False,
|
||||||
**kwargs) -> None:
|
**kwargs) -> None:
|
||||||
|
|
||||||
extra = {'_allow_range': allow_range,
|
extra = {'_allow_range': allow_range,
|
||||||
|
'_allow_protocol': allow_protocol,
|
||||||
'_min_value': None,
|
'_min_value': None,
|
||||||
'_max_value': None}
|
'_max_value': None,
|
||||||
|
}
|
||||||
ports_min = [0, 1, 1024, 49152]
|
ports_min = [0, 1, 1024, 49152]
|
||||||
ports_max = [0, 1023, 49151, 65535]
|
ports_max = [0, 1023, 49151, 65535]
|
||||||
is_finally = False
|
is_finally = False
|
||||||
|
|
@ -81,7 +84,9 @@ class PortOption(StrOption):
|
||||||
def validate(self,
|
def validate(self,
|
||||||
value: str) -> None:
|
value: str) -> None:
|
||||||
super().validate(value)
|
super().validate(value)
|
||||||
if self.impl_get_extra('_allow_range') and ":" in str(value):
|
if self.impl_get_extra('_allow_protocol') and (value.startswith('tcp:') or value.startswith('udp:')):
|
||||||
|
value = [value[4:]]
|
||||||
|
elif self.impl_get_extra('_allow_range') and ":" in str(value):
|
||||||
value = value.split(':')
|
value = value.split(':')
|
||||||
if len(value) != 2:
|
if len(value) != 2:
|
||||||
raise ValueError(_('range must have two values only'))
|
raise ValueError(_('range must have two values only'))
|
||||||
|
|
@ -98,7 +103,13 @@ class PortOption(StrOption):
|
||||||
def second_level_validation(self,
|
def second_level_validation(self,
|
||||||
value: str,
|
value: str,
|
||||||
warnings_only: bool) -> None:
|
warnings_only: bool) -> None:
|
||||||
for val in value.split(':'):
|
if self.impl_get_extra('_allow_protocol') and (value.startswith('tcp:') or value.startswith('udp:')):
|
||||||
|
value = [value[4:]]
|
||||||
|
elif ':' in value:
|
||||||
|
value = value.split(':')
|
||||||
|
else:
|
||||||
|
value = [value]
|
||||||
|
for val in value:
|
||||||
val = int(val)
|
val = int(val)
|
||||||
if not self.impl_get_extra('_min_value') <= val <= self.impl_get_extra('_max_value'):
|
if not self.impl_get_extra('_min_value') <= val <= self.impl_get_extra('_max_value'):
|
||||||
if warnings_only:
|
if warnings_only:
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,7 @@ class SynDynOptionDescription:
|
||||||
return rootpath + self.impl_getname()
|
return rootpath + self.impl_getname()
|
||||||
|
|
||||||
def impl_get_display_name(self) -> str:
|
def impl_get_display_name(self) -> str:
|
||||||
return self.opt.impl_get_display_name() + self._suffix
|
return self.opt.impl_get_display_name() + str(self._suffix)
|
||||||
|
|
||||||
|
|
||||||
class SynDynLeadership(SynDynOptionDescription):
|
class SynDynLeadership(SynDynOptionDescription):
|
||||||
|
|
|
||||||
|
|
@ -307,8 +307,8 @@ class Values:
|
||||||
"convenience method to know if an option is empty"
|
"convenience method to know if an option is empty"
|
||||||
empty = opt._empty
|
empty = opt._empty
|
||||||
if index in [None, undefined] and opt.impl_is_multi():
|
if index in [None, undefined] and opt.impl_is_multi():
|
||||||
isempty = value is None or (not force_allow_empty_list and value == []) or \
|
isempty = value is None or (isinstance(value, list) and not force_allow_empty_list and value == []) or \
|
||||||
None in value or empty in value
|
(isinstance(value, list) and None in value) or empty in value
|
||||||
else:
|
else:
|
||||||
isempty = value is None or value == empty or (opt.impl_is_submulti() and value == [])
|
isempty = value is None or value == empty or (opt.impl_is_submulti() and value == [])
|
||||||
return isempty
|
return isempty
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue