From ece7537b89c61aa5f3b98d39db16510abb8f5b4c Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Sun, 13 Nov 2022 15:04:12 +0100 Subject: [PATCH] rst to md doc conversion --- README.rst => README.md | 8 +- doc/README.md | 64 +++++ doc/api_value.md | 439 ++++++++++++++++++++++++++++++++ doc/browse.md | 350 +++++++++++++++++++++++++ doc/config.md | 107 ++++++++ doc/config.png | Bin 0 -> 15539 bytes doc/dynoptiondescription.md | 57 +++++ doc/gettingstarted.md | 42 +++ doc/leadership.md | 45 ++++ doc/option.md | 134 ++++++++++ doc/optiondescription.md | 35 +++ doc/options.md | 485 +++++++++++++++++++++++++++++++++++ doc/own_option.md | 183 +++++++++++++ doc/property.md | 109 ++++++++ doc/symlinkoption.md | 13 + doc/validator.md | 494 ++++++++++++++++++++++++++++++++++++ logo.png | Bin 0 -> 7732 bytes logo.svg | 140 ++++++++++ tiramisu/api.py | 25 +- 19 files changed, 2721 insertions(+), 9 deletions(-) rename README.rst => README.md (58%) create mode 100644 doc/README.md create mode 100644 doc/api_value.md create mode 100644 doc/browse.md create mode 100644 doc/config.md create mode 100644 doc/config.png create mode 100644 doc/dynoptiondescription.md create mode 100644 doc/gettingstarted.md create mode 100644 doc/leadership.md create mode 100644 doc/option.md create mode 100644 doc/optiondescription.md create mode 100644 doc/options.md create mode 100644 doc/own_option.md create mode 100644 doc/property.md create mode 100644 doc/symlinkoption.md create mode 100644 doc/validator.md create mode 100644 logo.png create mode 100644 logo.svg 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 0000000000000000000000000000000000000000..a468275da56ecf74534d340d77374e631f4deaae GIT binary patch literal 15539 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..154e27d77c27bff9bde8afbba41744366fa6089e GIT binary patch literal 7732 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 literal 0 HcmV?d00001 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