diff --git a/README.rst b/README.md similarity index 58% rename from README.rst rename to README.md index fdb5582..597532c 100644 --- a/README.rst +++ b/README.md @@ -1,5 +1,9 @@ -LICENSES ---------- +![Logo Tiramisu](logo.png "logo Tiramisu") + +[Documentations](doc/README.md) + + +# LICENSES See COPYING for the licences of the code and the documentation. diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 0000000..3f93181 --- /dev/null +++ b/doc/README.md @@ -0,0 +1,64 @@ +![Logo Tiramisu](../logo.png "logo Tiramisu") + +# Python3 Tiramisu library user documentation + +## The tasting of `Tiramisu` --- `user documentation` + +Tiramisu: + +- is a cool, refreshing Italian dessert, +- it is also an [options controller tool](http://en.wikipedia.org/wiki/Configuration_management#Overview) + +It's a pretty small, local (that is, straight on the operating system) options handler and controller. + +- [Getting started](gettingstarted.md) +- [The Config](config.md) +- [Browse the Config](browse.md) +- [Manage values](api_value.md) + +.. toctree:: + :maxdepth: 2 + + api_property + storage + application + quiz + glossary + +External project: + +.. toctree:: + :maxdepth: 2 + + cmdline_parser + +.. FIXME ca veut rien dire : "AssertionError: type 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` diff --git a/doc/api_value.md b/doc/api_value.md new file mode 100644 index 0000000..08ce38d --- /dev/null +++ b/doc/api_value.md @@ -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. + diff --git a/doc/browse.md b/doc/browse.md new file mode 100644 index 0000000..1a96739 --- /dev/null +++ b/doc/browse.md @@ -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: + +``` +[, ] +``` + +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: + +``` + +``` + +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). + diff --git a/doc/config.md b/doc/config.md new file mode 100644 index 0000000..6e91135 --- /dev/null +++ b/doc/config.md @@ -0,0 +1,107 @@ +# The Config + +Tiramisu is made of almost three main classes/concepts : + +- the "Option" stands for the option types +- the "OptionDescription" is the schema, the option's structure +- the "Config" which is the whole configuration entry point + +![The Config](config.png "The Config") + +## The handling of options + +The handling of options is split into two parts: the description of +which options are available, what their possible values and defaults are +and how they are organized into a tree. A specific choice of options is +bundled into a configuration object which has a reference to its option +description (and therefore makes sure that the configuration values +adhere to the option description). + +- [Instanciate an option](option.md) +- [Default Options](options.md) +- [The symbolic link option: SymLinkOption](symlinkoption.md) +- [Create it's own option](own_option.md) + +## Option description are nested Options + +The Option (in this case the "BoolOption"), +are organized into a tree into nested "OptionDescription" objects. + +Every option has a name, as does every option group. + +- [Generic container: OptionDescription](optiondescription.md) +- [Dynamic option description: DynOptionDescription](dynoptiondescription.md) +- [Leadership OptionDescription: Leadership](leadership.md) + +## Config + +Getting started with the tiramisu library (it loads and prints properly). + +Let's perform a *Getting started* code review : + +```python +from asyncio import run +from tiramisu import Config +from tiramisu import OptionDescription, BoolOption + +# let's create a group of options named "optgroup" +descr = OptionDescription("optgroup", "", [ + # ... with only one option inside + BoolOption("bool", "", default=False) + ]) + +async def main(): + # Then, we make a Config with the OptionDescription` we + cfg = await Config(descr) + # the global help about the config + cfg.help() + + +run(main()) +``` + +returns: + +``` +Root config object that enables us to handle the configuration options + +Settings: + forcepermissive Access to option without verifying permissive properties + unrestraint Access to option without property restriction + +Commands: + cache Manage config cache + config Actions to Config + information Manage config informations + option Select an option + owner Global owner + permissive Manage config permissives + property Manage config properties + session Manage Config session + value Manage config value +``` + +Then let's print our "Option details. + +```python +cfg.option.help() +``` + +returns: + +``` +Select an option + +Call: Select an option by path + +Commands: + dict Convert config and option to tiramisu format + find Find an or a list of options + list List options (by default list only option) + updates Updates value with tiramisu format +``` + +## Go futher with "Option" and "Config" + +- [property](property.md) +- [validator](validator.md) diff --git a/doc/config.png b/doc/config.png new file mode 100644 index 0000000..a468275 Binary files /dev/null and b/doc/config.png differ diff --git a/doc/dynoptiondescription.md b/doc/dynoptiondescription.md new file mode 100644 index 0000000..c5ca557 --- /dev/null +++ b/doc/dynoptiondescription.md @@ -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). + diff --git a/doc/gettingstarted.md b/doc/gettingstarted.md new file mode 100644 index 0000000..a0e0898 --- /dev/null +++ b/doc/gettingstarted.md @@ -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". diff --git a/doc/leadership.md b/doc/leadership.md new file mode 100644 index 0000000..aec8736 --- /dev/null +++ b/doc/leadership.md @@ -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). diff --git a/doc/option.md b/doc/option.md new file mode 100644 index 0000000..f89429d --- /dev/null +++ b/doc/option.md @@ -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) diff --git a/doc/optiondescription.md b/doc/optiondescription.md new file mode 100644 index 0000000..1857246 --- /dev/null +++ b/doc/optiondescription.md @@ -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). + diff --git a/doc/options.md b/doc/options.md new file mode 100644 index 0000000..0644683 --- /dev/null +++ b/doc/options.md @@ -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') +``` + + diff --git a/doc/own_option.md b/doc/own_option.md new file mode 100644 index 0000000..de85127 --- /dev/null +++ b/doc/own_option.md @@ -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) +``` diff --git a/doc/property.md b/doc/property.md new file mode 100644 index 0000000..345e22f --- /dev/null +++ b/doc/property.md @@ -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. diff --git a/doc/symlinkoption.md b/doc/symlinkoption.md new file mode 100644 index 0000000..d870bf7 --- /dev/null +++ b/doc/symlinkoption.md @@ -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) +``` diff --git a/doc/validator.md b/doc/validator.md new file mode 100644 index 0000000..2be6042 --- /dev/null +++ b/doc/validator.md @@ -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()) +``` diff --git a/logo.png b/logo.png new file mode 100644 index 0000000..154e27d Binary files /dev/null and b/logo.png differ diff --git a/logo.svg b/logo.svg new file mode 100644 index 0000000..d9211a8 --- /dev/null +++ b/logo.svg @@ -0,0 +1,140 @@ + + + + + + + + IRAMISU + + + + + + + + + + + + diff --git a/tiramisu/api.py b/tiramisu/api.py index bc5ff59..7a1ecda 100644 --- a/tiramisu/api.py +++ b/tiramisu/api.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . # ____________________________________________________________ -from inspect import ismethod, getdoc, signature +from inspect import ismethod, getdoc, signature, iscoroutinefunction from time import time from typing import List, Set, Any, Optional, Callable, Union, Dict from warnings import catch_warnings, simplefilter @@ -71,6 +71,9 @@ class TiramisuHelp: display(_('Commands:')) for module_name in modules: 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)) display(self._tmpl_help.format(module_name, doc).expandtabs(max_len + 10)) display() @@ -231,6 +234,7 @@ def option_and_connection(func): ret = await func(self, *args, **kwargs) del config_bag.connection return ret + wrapped.func = func return wrapped @@ -661,6 +665,7 @@ def option_type(typ): ret = await func(*args, **kwargs) del config_bag.connection return ret + wrapped.func = func return wrapped return wrapper @@ -865,7 +870,7 @@ class TiramisuOption(CommonTiramisu, TiramisuConfig): value=undefined, type=None, first: bool=False): - """find an option by name (only for optiondescription)""" + """Find an option by name (only for optiondescription)""" if not first: ret = [] option = self._option_bag.option @@ -967,7 +972,7 @@ class TiramisuOption(CommonTiramisu, TiramisuConfig): remotable: str="minimum", form: List=[], 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: await self._load_dict(clearable, remotable) return await self._tiramisu_dict.todict(form) @@ -975,7 +980,7 @@ class TiramisuOption(CommonTiramisu, TiramisuConfig): @option_type('optiondescription') async def updates(self, body: List) -> Dict: - """updates value with tiramisu format""" + """Updates value with tiramisu format""" if self._tiramisu_dict is None: await self._load_dict() return await self._tiramisu_dict.set_updates(body) @@ -989,6 +994,7 @@ def connection(func): ret = await func(self, *args, **kwargs) del config_bag.connection return ret + wrapped.func = func return wrapped @@ -1447,14 +1453,14 @@ class TiramisuContextOption(TiramisuConfig, _TiramisuOptionWalk): remotable="minimum", form=[], force=False): - """convert config and option to tiramisu format""" + """Convert config and option to tiramisu format""" if force or self._tiramisu_dict is None: await self._load_dict(clearable, remotable) return await self._tiramisu_dict.todict(form) async def updates(self, body: List) -> Dict: - """updates value with tiramisu format""" + """Updates value with tiramisu format""" if self._tiramisu_dict is None: await self._load_dict() return await self._tiramisu_dict.set_updates(body) @@ -1565,7 +1571,7 @@ class _TiramisuContextGroupConfig(TiramisuConfig): def __call__(self, path: Optional[str]): - """select a child Tiramisu config""" + """Select a child Tiramisu config""" spaths = path.split('.') config = self._config_bag.context for spath in spaths: @@ -1689,14 +1695,19 @@ class _TiramisuContextMetaConfig(_TiramisuContextMixConfig): class TiramisuContextCache(TiramisuConfig): + """Manage config cache""" + async def reset(self): + """Reset cache""" await self._config_bag.context.cfgimpl_reset_cache(None, None) async def set_expiration_time(self, time: int) -> None: + """Change expiration time value""" self._config_bag.expiration_time = time async def get_expiration_time(self) -> int: + """Get expiration time value""" return self._config_bag.expiration_time