From 93fa26f8df48c3ea8e8c88dcb8ade7b2894b3759 Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Sun, 17 Dec 2023 21:22:52 +0100 Subject: [PATCH] feat: documentation --- .readthedocs.yaml | 32 + doc/browse.md | 350 --------- doc/config.md | 107 --- doc/dynoptiondescription.md | 57 -- doc/leadership.md | 45 -- doc/option.md | 134 ---- doc/optiondescription.md | 35 - doc/options.md | 501 ------------ doc/own_option.md | 183 ----- doc/property.md | 109 --- doc/symlinkoption.md | 13 - doc/validator.md | 494 ------------ docs/Makefile | 23 + docs/_static/css/custom.css | 4 + docs/_static/python-logo-large.png | Bin 0 -> 13093 bytes docs/api_global_permissives.rst | 93 +++ docs/api_global_properties.rst | 185 +++++ docs/api_option_permissive.rst | 17 + docs/api_option_property.rst | 403 ++++++++++ docs/api_property.rst | 31 + doc/api_value.md => docs/api_value.rst | 215 +++--- docs/application.rst | 119 +++ docs/browse.rst | 159 ++++ docs/calculation.rst | 129 ++++ docs/cmdline_parser.rst | 718 ++++++++++++++++++ docs/conf.py | 149 ++++ {doc => docs}/config.png | Bin docs/config.rst | 138 ++++ docs/custom/static/custom.css | 39 + docs/custom/theme.conf | 4 + docs/dynoptiondescription.rst | 59 ++ .../gettingstarted.rst | 41 +- docs/glossary.rst | 84 ++ docs/images/firefox_preferences.png | Bin 0 -> 83071 bytes doc/README.md => docs/index.rst | 49 +- docs/leadership.rst | 48 ++ docs/logo.png | Bin 0 -> 2591 bytes docs/option.rst | 136 ++++ docs/optiondescription.rst | 38 + docs/options.rst | 330 ++++++++ docs/own_option.rst | 125 +++ docs/property.rst | 126 +++ docs/quiz.rst | 158 ++++ docs/requirements.txt | 81 ++ docs/src/Makefile | 18 + docs/src/api_global_permissive.py | 28 + docs/src/api_global_property.py | 20 + docs/src/api_option_property.py | 147 ++++ docs/src/api_value.py | 31 + docs/src/api_value_choice.py | 33 + docs/src/api_value_leader.py | 34 + docs/src/api_value_multi.py | 34 + docs/src/application.py | 232 ++++++ docs/src/calculation.py | 73 ++ docs/src/find.py | 6 + docs/src/getting_started.py | 19 + docs/src/own_option.py | 11 + docs/src/own_option2.py | 43 ++ docs/src/property.py | 20 + docs/src/proxy.py | 51 ++ docs/src/proxy_persistent.py | 54 ++ docs/src/quiz.py | 132 ++++ docs/src/validator.py | 133 ++++ docs/src/validator_follower.py | 41 + docs/src/validator_multi.py | 44 ++ docs/storage.png | Bin 0 -> 15867 bytes docs/storage.svg | 265 +++++++ docs/symlinkoption.rst | 13 + docs/validator.rst | 271 +++++++ 69 files changed, 5335 insertions(+), 2179 deletions(-) create mode 100644 .readthedocs.yaml delete mode 100644 doc/browse.md delete mode 100644 doc/config.md delete mode 100644 doc/dynoptiondescription.md delete mode 100644 doc/leadership.md delete mode 100644 doc/option.md delete mode 100644 doc/optiondescription.md delete mode 100644 doc/options.md delete mode 100644 doc/own_option.md delete mode 100644 doc/property.md delete mode 100644 doc/symlinkoption.md delete mode 100644 doc/validator.md create mode 100644 docs/Makefile create mode 100644 docs/_static/css/custom.css create mode 100644 docs/_static/python-logo-large.png create mode 100644 docs/api_global_permissives.rst create mode 100644 docs/api_global_properties.rst create mode 100644 docs/api_option_permissive.rst create mode 100644 docs/api_option_property.rst create mode 100644 docs/api_property.rst rename doc/api_value.md => docs/api_value.rst (73%) create mode 100644 docs/application.rst create mode 100644 docs/browse.rst create mode 100644 docs/calculation.rst create mode 100644 docs/cmdline_parser.rst create mode 100644 docs/conf.py rename {doc => docs}/config.png (100%) create mode 100644 docs/config.rst create mode 100644 docs/custom/static/custom.css create mode 100644 docs/custom/theme.conf create mode 100644 docs/dynoptiondescription.rst rename doc/gettingstarted.md => docs/gettingstarted.rst (61%) create mode 100644 docs/glossary.rst create mode 100644 docs/images/firefox_preferences.png rename doc/README.md => docs/index.rst (64%) create mode 100644 docs/leadership.rst create mode 100644 docs/logo.png create mode 100644 docs/option.rst create mode 100644 docs/optiondescription.rst create mode 100644 docs/options.rst create mode 100644 docs/own_option.rst create mode 100644 docs/property.rst create mode 100644 docs/quiz.rst create mode 100644 docs/requirements.txt create mode 100644 docs/src/Makefile create mode 100644 docs/src/api_global_permissive.py create mode 100644 docs/src/api_global_property.py create mode 100644 docs/src/api_option_property.py create mode 100644 docs/src/api_value.py create mode 100644 docs/src/api_value_choice.py create mode 100644 docs/src/api_value_leader.py create mode 100644 docs/src/api_value_multi.py create mode 100644 docs/src/application.py create mode 100644 docs/src/calculation.py create mode 100644 docs/src/find.py create mode 100644 docs/src/getting_started.py create mode 100644 docs/src/own_option.py create mode 100644 docs/src/own_option2.py create mode 100644 docs/src/property.py create mode 100644 docs/src/proxy.py create mode 100644 docs/src/proxy_persistent.py create mode 100644 docs/src/quiz.py create mode 100644 docs/src/validator.py create mode 100644 docs/src/validator_follower.py create mode 100644 docs/src/validator_multi.py create mode 100644 docs/storage.png create mode 100644 docs/storage.svg create mode 100644 docs/symlinkoption.rst create mode 100644 docs/validator.rst diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..71849fb --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,32 @@ +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the OS, Python version and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.12" + # You can also specify other tool versions: + # nodejs: "19" + # rust: "1.64" + # golang: "1.19" + +# Build documentation in the "docs/" directory with Sphinx +sphinx: + configuration: docs/conf.py + +# Optionally build your docs in additional formats such as PDF and ePub +# formats: +# - pdf +# - epub + +# Optional but recommended, declare the Python requirements required +# to build your documentation +# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +python: + install: + - requirements: docs/requirements.txt diff --git a/doc/browse.md b/doc/browse.md deleted file mode 100644 index 1a96739..0000000 --- a/doc/browse.md +++ /dev/null @@ -1,350 +0,0 @@ -# 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 deleted file mode 100644 index 6e91135..0000000 --- a/doc/config.md +++ /dev/null @@ -1,107 +0,0 @@ -# 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/dynoptiondescription.md b/doc/dynoptiondescription.md deleted file mode 100644 index c5ca557..0000000 --- a/doc/dynoptiondescription.md +++ /dev/null @@ -1,57 +0,0 @@ -# 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/leadership.md b/doc/leadership.md deleted file mode 100644 index aec8736..0000000 --- a/doc/leadership.md +++ /dev/null @@ -1,45 +0,0 @@ -# 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 deleted file mode 100644 index f89429d..0000000 --- a/doc/option.md +++ /dev/null @@ -1,134 +0,0 @@ -# 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 deleted file mode 100644 index 1857246..0000000 --- a/doc/optiondescription.md +++ /dev/null @@ -1,35 +0,0 @@ -# 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 deleted file mode 100644 index 1598a0d..0000000 --- a/doc/options.md +++ /dev/null @@ -1,501 +0,0 @@ -# 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') -``` - -## Unix file permissions: PermissionsOption - -Valid the representing Unix permissions is an octal (base-8) notation. - -```python -from tiramisu import PermissionsOption -PermissionsOption('perms', 'perms', 755) -PermissionsOption('perms', 'perms', 1755) -``` - -This option doesn't allow (or display a warning with warnings_only): - -- 777 (two weak value) -- others have more right than group -- group has more right than user - -# 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 deleted file mode 100644 index de85127..0000000 --- a/doc/own_option.md +++ /dev/null @@ -1,183 +0,0 @@ -# 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 deleted file mode 100644 index 345e22f..0000000 --- a/doc/property.md +++ /dev/null @@ -1,109 +0,0 @@ -# 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 deleted file mode 100644 index d870bf7..0000000 --- a/doc/symlinkoption.md +++ /dev/null @@ -1,13 +0,0 @@ -# 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 deleted file mode 100644 index 2be6042..0000000 --- a/doc/validator.md +++ /dev/null @@ -1,494 +0,0 @@ -# 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/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d845527 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,23 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SPHINXPROJ = pyfun +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + mkdir -p _build/html/_modules + make -C ../src all + diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css new file mode 100644 index 0000000..0d59fca --- /dev/null +++ b/docs/_static/css/custom.css @@ -0,0 +1,4 @@ +.wy-table-responsive table td { + white-space: normal; +} + diff --git a/docs/_static/python-logo-large.png b/docs/_static/python-logo-large.png new file mode 100644 index 0000000000000000000000000000000000000000..e3df4dedd88d07c0d2667532b05a932d78c30507 GIT binary patch literal 13093 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 literal 0 HcmV?d00001 diff --git a/docs/api_global_permissives.rst b/docs/api_global_permissives.rst new file mode 100644 index 0000000..e5ea211 --- /dev/null +++ b/docs/api_global_permissives.rst @@ -0,0 +1,93 @@ +================== +Global permissive +================== + +Permissives allow access, during a calculation, to a normally unavailable variable. + +In the :doc:api_property example we add a new `create` option that has a calculation with the option `exists` as parameter. + +This option has a calculated default_multi value. If the file exists (so `exists` option is True) we don't want create automaticly the file: + +.. literalinclude:: src/api_global_permissive.py + :lines: 7-8 + :linenos: + +Here is the new option: + +.. literalinclude:: src/api_global_permissive.py + :lines: 20-23 + :linenos: + +:download:`download the config ` + +Get/add/pop/reset global permissive +===================================== + +Let's try this config: + +>>> config.property.read_write() +>>> config.option('new.filename').value.set(['/etc', '/unknown']) +>>> config.value.get() +{'new.filename': ['/etc', '/unknown'], 'new.exists': [True, False], 'new.create': [False, True]} + +Now we want to see `advanced` option. But how calculate create value? + +>>> config.property.add('advanced') +>>> try: +... config.value.get() +... except ConfigError as err: +... print(err) +unable to carry out a calculation for "Create automaticly the file", cannot access to option "This file exists" because has property "advanced" + +We just have to add `advanced` permissive to allow calculation: + +>>> config.permissive.add('advanced') +>>> config.value.get() +{'new.filename': ['/etc', '/unknown'], 'new.create': [False, True]} + +At any time we can retrieve all global permissive: + +>>> config.permissive.get() +frozenset({'hidden', 'advanced'}) + +We can remove on permissive: + +>>> config.permissive.pop('hidden') +>>> config.permissive.get() +frozenset({'advanced'}) + +And finally we can reset all permissives: + +>>> config.permissive.reset() +>>> config.permissive.get() +frozenset() + +Default permissives +============================ + +Tiramisu estimate default permissive. + +All properties added in `read write` mode and removed in `read only` mode are, by default, included in permissive list when we change mode: + +>>> default = config.property.getdefault('read_write', 'append') +>>> config.property.setdefault(frozenset(default | {'advanced'}), 'read_write', 'append') +>>> default = config.property.getdefault('read_only', 'remove') +>>> config.property.setdefault(frozenset(default | {'advanced'}), 'read_only', 'remove') +>>> config.property.read_write() +>>> config.permissive.get() +frozenset({'advanced', 'hidden'}) + +Importation and exportation +================================ + +In config, all permissive (global's and option's permissives) can be exportated: + +>>> config.permissive.exportation() +{None: frozenset({'hidden', 'advanced'})} + +And reimported later: + +>>> export = config.permissive.exportation() +>>> config.permissive.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/docs/api_global_properties.rst b/docs/api_global_properties.rst new file mode 100644 index 0000000..275a484 --- /dev/null +++ b/docs/api_global_properties.rst @@ -0,0 +1,185 @@ +================ +Global property +================ + +Before start, have a look to :doc:property. + +Let's start by import needed objects: + +.. literalinclude:: src/api_global_property.py + :lines: 1-4 + :linenos: + +Instanciate a first option with `mandatory` property: + +.. literalinclude:: src/api_global_property.py + :lines: 7-10 + :linenos: + +Instanciate a second option with calculated value, which verify if the file exists. + +This options has `frozen`, `force_default_on_freeze` and `advanced` properties: + +.. literalinclude:: src/api_global_property.py + :lines: 11-15 + :linenos: + +This two options are in a leadership: + +.. literalinclude:: src/api_global_property.py + :lines: 16-18 + :linenos: + +Finally create the root option description and the config: + +.. literalinclude:: src/api_global_property.py + :lines: 19-20 + :linenos: + +:download:`download the config ` + +Read only and read write +========================== + +By default, there is no restriction. + +For example, it's possible to change value of a `frozen` option (here the `exists`' option): + +>>> config.option('new.filename').value.set(['/etc']) +>>> config.option('new.exists', 0).value.set(False) + +To have the good properties in "read / write" mode: + +>>> config.property.read_write() +>>> config.option('new.filename').value.set(['/etc']) +>>> try: +... config.option('new.exists', 0).value.set(False) +...except PropertiesOptionError as err: +... print(err) +cannot modify the option "This file exists" because has property "frozen" + +The read write mode is used be a human who wants to modify the configuration. +Some variables are not displayed, because this person cannot modified it. + +To have the good properties in "read only" mode: + +>>> config.property.read_only() +>>> try: +... config.option('new.filename').value.set(['/etc']) +...except PropertiesOptionError as err: +... print(err) +cannot modify the option "Filename" because has property "frozen" + +In this mode it is impossible to modify the values of the options. +It should be use by a script, for build a template, ... +All variables not desactived are accessible. + +Get/add/pop/reset global property +======================================= + +The default read only and read write properties are: + +>>> config.property.read_only() +>>> config.property.get() +frozenset({'force_store_value', 'validator', 'everything_frozen', 'warnings', 'cache', 'mandatory', 'frozen', 'empty', 'disabled'}) +>>> config.property.read_write() +>>> config.property.get() +frozenset({'frozen', 'cache', 'warnings', 'disabled', 'validator', 'force_store_value', 'hidden'}) + +In the current config, the option has property `advanced`. + +Has you can see below, the `advanced` is not used in any mode. This property doesn't affect Tiramisu. + +Imagine that you don't want to see any advanced option by default. Just add this property in global property: + +>>> config.option('new.filename').value.set(['/etc']) +>>> config.property.read_write() +>>> config.value.get() +{'new.filename': ['/etc'], 'new.exists': [True]} +>>> config.property.add('advanced') +>>> config.property.get() +frozenset({'frozen', 'advanced', 'hidden', 'validator', 'force_store_value', 'disabled', 'cache', 'warnings'}) +>>> config.value.get() +{'new.filename': ['/etc']} + +Of course you want to access to this option in read only mode. +So you have to remove this property: + +>>> config.property.read_only() +>>> config.property.pop('advanced') +>>> config.property.get() +frozenset({'force_store_value', 'everything_frozen', 'frozen', 'warnings', 'empty', 'disabled', 'mandatory', 'cache', 'validator'}) +>>> config.value.get() +{'new.filename': ['/etc'], 'new.exists': [True]} + +At any time we can return to the default property (default means initialized properties, before change to read only or read write mode): + +>>> config.property.read_only() +>>> config.property.get() +frozenset({'empty', 'cache', 'force_store_value', 'everything_frozen', 'warnings', 'frozen', 'disabled', 'mandatory', 'validator'}) +>>> config.property.reset() +>>> config.property.get() +frozenset({'cache', 'warnings', 'validator'}) + +Get default properties in mode +======================================= + +Add or pop properties each time we pass from one mode to an other is not a good idea. It better to change `read_write` and `read_only` mode directly. + +Change mode means, in fact, add some properties and remove some other properties. + +For example, when we pass to read_write mode, this properties are added: + +>>> config.property.getdefault('read_write', 'append') +frozenset({'disabled', 'validator', 'force_store_value', 'hidden', 'frozen'}) + +and this properties are remove: + +>>> config.property.getdefault('read_write', 'remove') +frozenset({'empty', 'everything_frozen', 'mandatory', 'permissive'}) + +Here is properties added when pass to read_only mode: + +>>> config.property.getdefault('read_only', 'append') +frozenset({'empty', 'mandatory', 'validator', 'disabled', 'force_store_value', 'everything_frozen', 'frozen'}) + +and this properties are remove: + +>>> config.property.getdefault('read_only', 'remove') +frozenset({'hidden', 'permissive'}) + +Just add the property to the default value to automatically automate the addition and deletion. +We want to add the property when we switch to "read write" mode and automatically delete this property when we switch to "read only" mode: + +>>> default = config.property.getdefault('read_write', 'append') +>>> config.property.setdefault(frozenset(default | {'advanced'}), 'read_write', 'append') +>>> default = config.property.getdefault('read_only', 'remove') +>>> config.property.setdefault(frozenset(default | {'advanced'}), 'read_only', 'remove') + +Let's try: + +>>> 'advanced' in config.property.get() +False +>>> config.property.read_write() +>>> 'advanced' in config.property.get() +True +>>> config.property.read_only() +>>> 'advanced' in config.property.get() +False + +Importation and exportation +======================================= + +In config, all properties (global's and option's properties) can be exportated: + +>>> config.property.exportation() +{None: frozenset({'empty', 'cache', 'warnings', 'validator', 'disabled', 'force_store_value', 'everything_frozen', 'frozen', 'mandatory'})} + +And reimported later: + +>>> export = config.property.exportation() +>>> config.property.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/docs/api_option_permissive.rst b/docs/api_option_permissive.rst new file mode 100644 index 0000000..866bde8 --- /dev/null +++ b/docs/api_option_permissive.rst @@ -0,0 +1,17 @@ +===================== +Option's permissive +===================== + + +.. FIXME advanced in permissive + + +.. FIXME unrestraint, forcepermissive + + + + + + + + diff --git a/docs/api_option_property.rst b/docs/api_option_property.rst new file mode 100644 index 0000000..ea4b608 --- /dev/null +++ b/docs/api_option_property.rst @@ -0,0 +1,403 @@ +================== +Option's property +================== + +Let's start to build a config. + +First of, import needed object: + +.. literalinclude:: src/api_option_property.py + :lines: 1-9 + :linenos: + +Instanciate a first option to call a file name: + +.. literalinclude:: src/api_option_property.py + :lines: 56-59 + :linenos: + +Secondly add an `exists` option to know if this file is already created: + +.. literalinclude:: src/api_option_property.py + :lines: 60-64 + :linenos: + +Thirdly add a `create` option used by a potential script to create wanted file: + +.. literalinclude:: src/api_option_property.py + :lines: 65-72 + :linenos: + +A new option is create to known the file type. If file already exists, retrieve automaticly the type, otherwise ask to the user: + +.. literalinclude:: src/api_option_property.py + :lines: 35-42 + :linenos: + +.. literalinclude:: src/api_option_property.py + :lines: 73-87 + :linenos: + +In same model, create a `user` and `group` name options: + +.. literalinclude:: src/api_option_property.py + :lines: 12-32 + :linenos: + +.. literalinclude:: src/api_option_property.py + :lines: 88-111 + :linenos: + +Finally create a `mode` option: + +.. literalinclude:: src/api_option_property.py + :lines: 45-53 + :linenos: + +.. literalinclude:: src/api_option_property.py + :lines: 112-116 + :linenos: + +Let's build the config: + +.. literalinclude:: src/api_option_property.py + :lines: 118-124 + :linenos: + +:download:`download the config ` + +Get/add/pop/reset property +================================= + +option description's property +''''''''''''''''''''''''''''''''''''''''' + +An option description is an option. It's possible to set property to it: + +>>> config.property.read_write() +>>> config.option('new').property.get() +set() + +To add a property: + +>>> config.option('new').property.add('disabled') +>>> config.option('new').property.get() +set('disabled') + +The property affect the option description: + +>>> try: +... config.option('new').value.get() +... except PropertiesOptionError as err: +... print(err) +cannot access to optiondescription "Add new file" because has property "disabled" + +But, of course the child option too. If access to option description is not possible, it's not possible to child option too: + +>>> try: +... config.option('new.filename').value.get() +... except PropertiesOptionError as err: +... print(err) +cannot access to optiondescription "Add new file" because has property "disabled" + +We can remove an existed property too: + +>>> config.option('new').property.add('hidden') +>>> config.option('new').property.get() +{'hidden', 'disabled'} +>>> config.option('new').property.pop('hidden') +>>> config.option('new').property.get() +{'disabled'} + + +It's possible to reset property: + +>>> config.option('new').property.reset() +>>> config.option('new').value.get() +{'filename': [], 'exists': [], 'create': [], 'type': [], 'user': [], 'group': [], 'mode': []} + +option's property +''''''''''''''''''''''''''''''''''''''' + +In a simple option we can add, pop or reset property: + +>>> config.property.read_write() +>>> config.option('new.filename').property.get()) +{'mandatory', 'unique', 'empty'} +>>> config.option('new.filename').property.add('frozen') +>>> config.option('new.filename').property.get() +{'mandatory', 'unique', 'frozen', 'empty'} +>>> config.option('new.filename').property.pop('empty') +>>> config.option('new.filename').property.get() +{'frozen', 'mandatory', 'unique'} +>>> config.option('new.filename').property.reset() +>>> config.option('new.filename').property.get() +{'mandatory', 'unique', 'empty'} + +leader's property +'''''''''''''''''''''''''''' + +In leader's option can only have a list of property. For other's property, please set directly in leadership option: + +>>> config.property.read_write() +>>> try: +... config.option('new.filename').property.add('hidden') +... except LeadershipError as err: +... print(err) +leader cannot have "hidden" property +>>> config.option('new').property.add('hidden') + +This `hidden` property has to affect leader option but also all follower option. +That why you have to set this kind of properties directly in leadership option. + +.. note:: + Allowed properties for a leader: 'empty', 'unique', 'force_store_value', 'mandatory', 'force_default_on_freeze', 'force_metaconfig_on_freeze', and 'frozen'. + + +follower's property +''''''''''''''''''''''''''''''''''''''' + +First of add, add values in leader option: + +>>> config.property.read_write() +>>> config.option('new.filename').value.set(['/etc/passwd', 'unknown1', 'unknown2']) + +We have to get property with an index: + +>>> config.option('new.create', 1).property.get() +set() + +We can set property with index: + +>>> config.option('new.create', 1).property.add('frozen') +>>> config.option('new.create', 1).property.get() +{'frozen'} +>>> config.option('new.create', 2).property.get() +set() + +But we can alse set without index (available for all follower's value): + +>>> config.option('new.create').property.add('frozen') +>>> print(config.option('new.create', 1).property.get()) +{'frozen'} +>>> print(config.option('new.create', 2).property.get()) +{'frozen'} + +Calculated property +======================= + +A property can be a :doc:`calculation`. That means that the property will be set or not following the context. + +The Calculation can return two type of value: + +- a `str` this string is a new property +- `None` so this property is cancel + +First of all, have a look to the `create` properties: + +.. literalinclude:: src/api_option_property.py + :lines: 68-72 + :linenos: + +This option has only one property which is `disabled` when `exists` has value True. + +If the file exists, we don't have to now if user wants create it. It is already exists. So we don't have to access to this option. + +Secondly, have a look to the `type` properties: + +.. literalinclude:: src/api_option_property.py + :lines: 79-87 + :linenos: + +There is: + +- two static properties: `force_default_on_freeze` and `mandatory`. +- two calculated properties: `hidden` and `frozen` + +If the file is already exists, the two calculated properties are present to this option. + +So we can access to this option only in read only mode and user cannot modified it's value. + +Finally have a look to the `username` and `grpname` options' properties: + +.. literalinclude:: src/api_option_property.py + :lines: 94-99 + :linenos: + +In this case we have two properties: + +- one static property: `force_store_value` +- one calculated property: `mandatory` + +This calculated property is apply only if `create` is True. + +Be carefull to the `create` option. It could be disabled, so not accessible in calculation if the file exists as see previously. + +That why we add notraisepropertyerror attribute to True, even if the calculation will failed. +In this case the value of `create` is not add in `calc_value` argument. + +In this case the function `calc_value` consider that the property `mandatory` has to be set. + +But we just want to set `mandatory` property only if create is False. That why we add the no_condition_is_invalid to True. + +Force the registration of a value +==================================== + +The property `force_store_value` is a special property. This property permit to store a value automaticly even if user do not set value or reset the value. +This is useful especially, for example, for recording a random draw password through a calculation. Or to store any first result for a calculation. + +To the, create a new config: + +>>> config = Config(root) + +If we add value in `filename`, the option `exists` stay a default value, but not the `mode` option, which has `force_store_value`: + +>>> config.property.read_write() +>>> config.option('new.filename').value.set(['/etc']) +>>> print(config.option('new.filename').owner.get()) +user +>>> print(config.option('new.exists', 0).owner.get()) +default +>>> print(config.option('new.mode', 0).owner.get()) +forced + +If we try to reset `mode` value, this option is modified: + +>>> config.option('new.mode', 0).value.reset() +>>> config.option('new.mode', 0).owner.get() +forced + +Non-empty value, mandatory and unique +======================================================== + +Leader and multi have automaticly two properties `unique` and `empty`: + +>>> config = Config(OptionDescription('root', 'root', [FilenameOption('filename', +... 'Filename', +... multi=True)])) +>>> config.option('filename').property.get() +{'empty', 'unique'} + +To remove `empty` property + +>>> config = Config(OptionDescription('root', 'root', [FilenameOption('filename', +... 'Filename', +... properties=('notempty',), +... multi=True)])) +>>> config.option('filename').property.get() +{'unique'} +>>> config = Config(OptionDescription('root', 'root', [FilenameOption('filename', +... 'Filename', +... properties=('notunique',), +... multi=True)])) +>>> config.option('filename').property.get() +{'empty'} + +Let's try with previous config. + +First of all we remove `force_store_value` mode: + +>>> config = Config(root) +>>> properties = config.property.getdefault('read_write', 'append') - {'force_store_value'} +>>> config.property.setdefault(frozenset(properties), 'read_write', 'append') +>>> properties = config.property.getdefault('read_only', 'append') - {'force_store_value'} +>>> config.property.setdefault(frozenset(properties), 'read_only', 'append') + +In addition to the specified `mandatory` property, leader have automaticly two properties: `unique` and `empty`: + +>>> config.option('new.filename').property.get() +{'unique', 'mandatory', 'empty'} + +What is the difference between the property `unique` and `mandatory`? + +Let's try with no value at all: + +>>> config.property.read_only() +>>> try: +... config.option('new.filename').value.get() +>>> except PropertiesOptionError as err: +... print(err) +cannot access to option "Filename" because has property "mandatory" + +A `mandatory` multi must have at least one value. This value is check only in read only mode. + +If we remove the `mandatory` property, the value is valid: + +>>> config.property.read_write() +>>> config.option('new.filename').property.pop('mandatory') +>>> config.option('new.filename').property.get() +{'unique', 'empty'} +>>> config.property.read_only() +>>> config.option('new.filename').value.get() +[] + +A `empty` multi can has no value, but if you set a value, it must not be None: + +>>> config.property.read_write() +>>> config.option('new.filename').value.set(['/etc', None]) +>>> config.property.read_only() +>>> try: +... config.option('new.filename').value.get() +... except PropertiesOptionError as err: +... print(err) +cannot access to option "Filename" because has property "empty" + +Trying now without this property: + +>>> config.property.read_write() +>>> config.option('new.filename').property.pop('empty') +>>> config.option('new.filename').value.set(['/etc', None]) +>>> config.property.read_only() +>>> config.option('new.filename').value.get() +['/etc', None] + +A `unique` property in multi means you cannot have same value twice: + +>>> config.property.read_write() +>>> try: +... config.option('new.filename').value.set(['/etc', '/etc']) +... except ValueError as err: +... print(err) +"['/etc', '/etc']" is an invalid file name for "Filename", the value "/etc" is not unique + +When removing this property: + +>>> config.property.read_write() +>>> config.option('new.filename').property.pop('unique') +>>> config.option('new.filename').value.set(['/etc', '/etc']) +>>> config.property.read_only() +>>> config.option('new.filename').value.get() +['/etc', '/etc'] + +Non-modifiable option +===================== + +Freeze an option means that you cannot change the value of this option: + +>>> config = Config(root) +>>> config.property.read_write() +>>> config.option('new.filename').value.set(['unknown']) +>>> config.option('new.create', 0).value.set(False) +>>> config.option('new.create', 0).property.add('frozen') +>>> try: +... config.option('new.create', 0).value.set(False) +... except PropertiesOptionError as err: +... print(err) +cannot modify the option "Create automaticly the file" because has property "frozen" + +Sometime (for example when an option is calculated) we want retrieve the default value (so the calculated value) when we add `frozen` option. + +In the current example, `new.exists` is a calculated value and we don't want that the used modify this option. So we add `frozen` and `force_default_on_freeze` properties. + +For example, without mode, we can modify the `new.exists` option, but in `read_only` mode, we want to have default value: + +>>> config = Config(root) +>>> config.option('new.filename').value.set(['unknown']) +>>> config.option('new.exists', 0).value.set(True) +>>> config.option('new.exists', 0).value.get() +True +>>> config.property.read_write() +>>> config.option('new.exists', 0).value.get() +False + +The property `force_default_on_freeze` is also avalaible in the option `new.type`. If the file exists, the type is calculated but if it not already exists, the user needs to set the correct wanted type. diff --git a/docs/api_property.rst b/docs/api_property.rst new file mode 100644 index 0000000..b61a16b --- /dev/null +++ b/docs/api_property.rst @@ -0,0 +1,31 @@ +================================== +Playing with property +================================== + +Properties and permissives affect the Tiramisu behaviour. + +This mechanism makes available or not other options. It also controls the behavior of options. + +.. toctree:: + :maxdepth: 2 + + api_global_properties + api_global_permissives + api_option_property + api_option_permissive + + + +.. class TiramisuContextValue(TiramisuConfig): +.. def mandatory(self): +.. """Return path of options with mandatory property without any value""" +.. + +.. class TiramisuOptionPermissive(CommonTiramisuOption): +.. """Manage option's permissive""" +.. def get(self): +.. """Get permissives value""" +.. def set(self, permissives): +.. """Set permissives value""" +.. def reset(self): +.. """Reset all personalised permissive""" diff --git a/doc/api_value.md b/docs/api_value.rst similarity index 73% rename from doc/api_value.md rename to docs/api_value.rst index bb288ef..9aa79bc 100644 --- a/doc/api_value.md +++ b/docs/api_value.rst @@ -1,8 +1,12 @@ -# Manage values +================================== +Manage values +================================== -## Values with options +Values with options +========================= -### Simple option +Simple option +---------------------------- Begin by creating a Config. This Config will contains two options: @@ -11,129 +15,92 @@ Begin by creating a Config. This Config will contains two options: 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 -``` +.. literalinclude:: src/api_value.py + :lines: 1-4 + :linenos: 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') -``` +.. literalinclude:: src/api_value.py + :lines: 6-9 + :linenos: 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()))]) -``` +.. literalinclude:: src/api_value.py + :lines: 24-25 + :linenos: 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 -``` +.. literalinclude:: src/api_value.py + :lines: 11-21 + :linenos: 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)))) -``` +.. literalinclude:: src/api_value.py + :lines: 26-27 + :linenos: Finally add those options in option description and a Config: -```python -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() - return config +.. literalinclude:: src/api_value.py + :lines: 28-31 + :linenos: -config = run(main()) -``` +:download:`download the config ` -#### Get and set a value +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: - -``` +>>> config.option('disk.path').value.get() None +>>> config.option('disk.usage').value.get() 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()) +>>> config.option('disk.path').value.set('/') -run(main()) -``` +The value is really change: -returns: - -``` +>>> config.option('disk.path').value.get() / + +Now, calculation retrieve a value: + +>>> config.option('disk.usage').value.get() 668520882176.0 -``` When you enter a value it is validated: -```python -async def main(): - try: - await config.option('disk.path').value.set('/unknown') - except ValueError as err: - print(err) - -run(main()) -``` - -returns: - -``` +>>> 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 -``` -#### Is value is valid? +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: -```python -await config.option('disk.path').value.valid() -``` +>>> config.option('disk.path').value.valid() +True -#### Display the default value +Display the default value +''''''''''''''''''''''''''''' Even if the value is modify, you can display the default value with `default` method: @@ -144,7 +111,8 @@ Even if the value is modify, you can display the default value with `default` me >>> config.option('disk.usage').value.default() 668510105600.0 -#### Return to the default value +Return to the default value +''''''''''''''''''''''''''''' If the value is modified, just `reset` it to retrieve the default value: @@ -155,7 +123,8 @@ If the value is modified, just `reset` it to retrieve the default value: >>> config.option('disk.path').value.get() None -#### The ownership of a value +The ownership of a value +''''''''''''''''''''''''''''' Every option has an owner, that will indicate who changed the option's value last. @@ -206,26 +175,28 @@ We can change this owner: >>> config.option('disk.path').owner.get() itsme -### Get choices from a Choice option +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 +.. 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>` +:download:`download the config ` 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 +Value in multi option +-------------------------------------- .. FIXME undefined @@ -237,25 +208,26 @@ 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 +.. 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 +.. literalinclude:: src/api_value_multi.py :lines: 11-26 :linenos: Finally `usage` option is also a multi: -.. literalinclude:: ../src/api_value_multi.py +.. literalinclude:: src/api_value_multi.py :lines: 27-30 :linenos: -:download:`download the config <../src/api_value_multi.py>` +:download:`download the config ` -#### Get or set a multi value +Get or set a multi value +''''''''''''''''''''''''''''' Since the options are multi, the default value is a list: @@ -272,7 +244,8 @@ A multi option waiting for a list: >>> config.option('disk.usage').value.get() [668499898368.0, 8279277568.0] -#### The ownership of multi option +The ownership of multi option +''''''''''''''''''''''''''''' There is no difference in behavior between a simple option and a multi option: @@ -285,7 +258,8 @@ default >>> config.option('disk.path').owner.get() user -### Leadership +Leadership +-------------------------------------- In previous example, we cannot define different `size_type` for each path. If you want do this, you need a leadership. @@ -295,23 +269,24 @@ As each value of followers are isolate, the function `calc_disk_usage` will rece So let's change this function: -.. literalinclude:: ../src/api_value_leader.py +.. literalinclude:: src/api_value_leader.py :lines: 12-18 :linenos: Secondly the option `size_type` became a multi: -.. literalinclude:: ../src/api_value_leader.py +.. literalinclude:: src/api_value_leader.py :lines: 24-25 :linenos: Finally disk has to be a leadership: -.. literalinclude:: ../src/api_value_leader.py +.. literalinclude:: src/api_value_leader.py :lines: 30 :linenos: -#### Get and set a leader +Get and set a leader +''''''''''''''''''''''''''''' A leader is, in fact, a multi option: @@ -346,7 +321,8 @@ To reduce use the `pop` method: >>> config.option('disk.path').value.get() ['/'] -#### Get and set a follower +Get and set a follower +''''''''''''''''''''''''''''' As followers are isolate, we cannot get all the follower values: @@ -376,7 +352,8 @@ 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 +The ownership of a leader and follower +''''''''''''''''''''''''''''''''''''''''''' There is no differences between a multi option and a leader option: @@ -396,37 +373,36 @@ True >>> config.option('disk.size_type', 1).owner.get() default -## Values in option description +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) +>>> config.option('disk').value.get() {'disk.path': ['/', '/tmp'], 'disk.size_type': ['giga bytes', 'bytes'], 'disk.usage': [622.578239440918, 8279273472.0]} -## Values in config +Values in config +========================== -###dict +get +-------- 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() +>>> config.value.get() {'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) +>>> config.value.get(flatten=True) {'path': ['/', '/tmp'], 'size_type': ['giga bytes', 'bytes'], 'usage': [622.578239440918, 8279273472.0]} -### importation/exportation +importation/exportation +------------------------ In config, we can export full values: @@ -439,4 +415,3 @@ and reimport it later: >>> 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/docs/application.rst b/docs/application.rst new file mode 100644 index 0000000..3074d84 --- /dev/null +++ b/docs/application.rst @@ -0,0 +1,119 @@ +================================== +A full application +================================== + +The firefox network configuration +------------------------------------- + +Now we are going to resume everything we have seen with a concrete example. +We're going to take an example based on the `Mozilla Firefox +`_ proxy's +configuration, like what is required when you open the `network settings` in +the General configuration's firefox page: + +.. image:: images/firefox_preferences.png + +The tiramisu's configuration +---------------------------- + +Build the `Config` +'''''''''''''''''''' + +First, let's create our options : + +.. literalinclude:: src/application.py + :lines: 1-3, 12-20 + :linenos: + +This first option is the most important one : its value will determine which other options +are disabled and which are not. The same thing will happen with other options later. +Here are the others options we'll be using : + +.. literalinclude:: src/application.py + :lines: 23-221 + :linenos: + +As you can see, we're using :doc:`value `, :doc:`property ` +and :doc:`calculation` in the setting of our options, because we have many options which +value or accessibility is depending on the value of other options. + +Now we need to create OptionDescriptions and configs : + +.. literalinclude:: src/application.py + :lines: 223-232 + :linenos: + +Download the :download:`full code ` of this example. + +As you can see, we regrouped a lot of options in 'protocols', so we can set a calculated `disabled` property +that is apply to all those options. This way, we don't have to put the instruction on every option +one by one. + +Let's try +''''''''''''''' + +Now that we have our Config, it's time to run some tests ! +Here are a few code blocks you can test and the results you should get : + +1. Automatic proxy configuration URL: + +>>> proxy_config.property.read_write() +>>> proxy_config.option('proxy_mode').value.set('Automatic proxy configuration URL') +>>> proxy_config.option('auto_config_url').value.set('http://192.168.1.1/wpad.dat') +>>> proxy_config.property.read_only() +>>> for path, value in proxy_config.value.get().items(): +... print(proxy_config.option(path).option.doc() + ': "' + str(value) + '"') +Proxy's config mode: "Automatic proxy configuration URL" +Address for which proxy will be desactivated: "[]" +Proxy's auto config URL: "http://192.168.1.1/wpad.dat" +Prompt for authentication if password is saved: "False" +Enable DNS over HTTPS: "False" + +2. Auto-detect proxy settings for this network: + +>>> proxy_config.property.read_write() +>>> proxy_config.option('proxy_mode').value.set('Auto-detect proxy settings for this network') +>>> proxy_config.option('no_proxy').value.set(['localhost', +... '127.0.0.1', +... '192.16.10.150', +... '192.168.5.101', +... '192.168.56.101/32', +... '192.168.20.0/24', +... '.tiramisu.org', +... 'mozilla.org']) +>>> proxy_config.option('dns_over_https.enable_dns_over_https').value.set(True) +>>> proxy_config.option('dns_over_https.used_dns').value.set('default') +>>> proxy_config.property.read_only() +>>> for path, value in proxy_config.value.get().items(): +... print(proxy_config.option(path).option.doc() + ': "' + str(value) + '"') +Proxy's config mode: "Auto-detect proxy settings for this network" +Address for which proxy will be desactivated: "['localhost', '127.0.0.1', '192.16.10.150', '192.168.5.101', '192.168.56.101/32', '192.168.20.0/24', '.tiramisu.org', 'mozilla.org']" +Prompt for authentication if password is saved: "False" +Enable DNS over HTTPS: "True" +Used DNS: "default" + +Set use_for_all_protocols to True: + +>>> proxy_config.property.read_write() +>>> proxy_config.option('protocols.use_for_all_protocols').value.set(True) +>>> proxy_config.property.read_only() +>>> for path, value in proxy_config.value.get().items(): +... print(proxy_config.option(path).option.doc() + ': "' + str(value) + '"') +Proxy's config mode: "Manual proxy configuration" +Address: "192.168.20.1" +Port: "8080" +Use HTTP IP and Port for all protocols: "True" +Address: "192.168.20.1" +Port: "8080" +Address: "192.168.20.1" +Port: "8080" +Address: "192.168.20.1" +Port: "8080" +SOCKS host version used by proxy: "v5" +Address for which proxy will be desactivated: "[]" +Prompt for authentication if password is saved: "False" +Use Proxy DNS when using SOCKS v5: "False" +Enable DNS over HTTPS: "True" +Used DNS: "custom" +Custom DNS URL: "https://dns-url.com" + diff --git a/docs/browse.rst b/docs/browse.rst new file mode 100644 index 0000000..738c197 --- /dev/null +++ b/docs/browse.rst @@ -0,0 +1,159 @@ +Browse the :class:`Config` +=========================== + +Getting the options +---------------------- + +.. note:: The :class:`Config` object we are using is located here in this script: + + :download:`download the source ` + +Let's retrieve the config object, named `cfg` + +.. code-block:: python + + from property import cfg + +We retrieve by path an option named `var1` +and then we retrieve its name and its docstring + +.. code-block:: bash + :emphasize-lines: 2, 5, 8 + + print(cfg.option('od1.var1')) + + + print(cfg.option('od1.var1').option.name()) + 'var1' + + print(cfg.option('od1.var1').option.doc()) + '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: + + +.. code-block:: bash + :emphasize-lines: 10, 14 + + # getting all the options + print(cfg.option.value.get()) + {'var1': None, 'var2': 'value'} + + # getting the `od1` option description + print(cfg.option('od1').value.get()) + {'od1.var1': None, 'od1.var2': 'value'} + + # getting the var1 option's value + print(cfg.option('od1.var1').value.get()) + None + + # getting the var2 option's default value + print(cfg.option('od1.var2').value.get()) + 'value' + + # trying to get a non existent option's value + cfg.option('od1.idontexist').value.get() + AttributeError: unknown option "idontexist" in optiondescription "od1" + + +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. + +.. code-block:: bash + :emphasize-lines: 2 + + # changing the `od1.var1` value + cfg.option('od1.var1').value.set('éééé') + print(cfg.option('od1.var1').value.get()) + 'éééé' + + # carefull to the type of the value to be set + cfg.option('od1.var1').value.set(23454) + ValueError: "23454" is an invalid string for "first variable" + + # let's come back to the default value + cfg.option('od1.var2').value.reset() + print(cfg.option('od1.var2').value.get()) + 'value' + +.. important:: If the config is `read only`, setting an option's value isn't allowed, see :doc:`property` + + +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 +:term:`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 :func:`find()` method. + +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 + +.. code-block:: bash + :emphasize-lines: 1, 6, 19 + + print(cfg.option.find(name='var1')) + # [, ] + + # If the option name is unique, the search can be stopped once one matched option + # has been found: + print(cfg.option.find(name='var1', first=True)) + #  + + # a search object behaves like a cfg object, for example + print(cfg.option.find(name='var1', first=True).option.name()) + # 'var1' + print(cfg.option.find(name='var1', first=True).option.doc()) + + # a search can be made with various criteria + print(cfg.option.find(name='var3', value=undefined)) + print(cfg.option.find(name='var3', type=StrOption)) + + # the find method can be used in subconfigs + print(cfg.option('od2').find('var1')) + +:download:`download the config used for the find ` + +The `get` flattening utility +------------------------------------- + +In a config or a subconfig, you can print a dict-like representation + +.. code-block:: bash + :emphasize-lines: 2 + + # get the `od1` option description + print(cfg.option('od1').value.get()) + {'od1.var1': 'éééé', 'od1.var2': 'value'} diff --git a/docs/calculation.rst b/docs/calculation.rst new file mode 100644 index 0000000..5587e35 --- /dev/null +++ b/docs/calculation.rst @@ -0,0 +1,129 @@ +================================== +Calculation +================================== + +Calculation is a generic object that allow you to call an external function. + +Simple calculation +================================== + +It's structure is the following : + +.. literalinclude:: src/calculation.py + :lines: 1-5 + :linenos: + +Positional and keyword arguments +========================================= + +Function's arguments are also specified in this object. +Let's see with a positional argument and a keyword argument: + +.. literalinclude:: src/calculation.py + :lines: 8-10 + :linenos: + +The function `a_function_with_parameters` will be call with the positional argument `value1` to `my value 1` and the keyword argument `value2` to `my value 2`. +So when this function will be executed, it will return `my value 1 my value 2`. + +Let's see with two positional arguments: + +.. literalinclude:: src/calculation.py + :lines: 13-15 + :linenos: + +As we have several positional arguments, the first Params' argument is a tuple. +This example will return strictly same result has previous example. + +Option has an argument +========================================= + +In previous examples, we use ParamValue arguments, which could contain random value. But this value is static and cannot be change. + +It could be interesting to use an existant option has an argument: + +.. literalinclude:: src/calculation.py + :lines: 18-21 + :linenos: + +As long as option1 is at its default value, the function will return `1`. If we set option1 to `12`, the function will return `12`. + +Pay attention to the properties when you use an option. +This example will raise a ConfigError: + +.. literalinclude:: src/calculation.py + :lines: 24-27 + :linenos: + +It's up to you to define the desired behavior. + +If you want the option to be transitively disabled just set the raisepropertyerror argument to True: + +.. literalinclude:: src/calculation.py + :lines: 29-31 + :linenos: + +If you want to remove option in argument, just set the notraisepropertyerror argument to True: + +.. literalinclude:: src/calculation.py + :lines: 33-35 + :linenos: + +In this case, option1 will not pass to function. You have to set a default value to this argument. +So, function will return `None`. + +In these examples, the function only accesses to the value of the option. But no additional information is given. +It is possible to add the parameter `todict` to `True` to have the description of the option in addition to its value. + +.. literalinclude:: src/calculation.py + :lines: 37-39 + :linenos: + +This function will return `the option first option has value 1`. + +Multi option has an argument +========================================= + +An option could be a multi. Here is an example: + +.. literalinclude:: src/calculation.py + :lines: 46-49 + :linenos: + +In this case the function will return the complete list. So `[1]` in this example. + +Leader or follower option has an argument +============================================ + +An option could be a leader: + +.. literalinclude:: src/calculation.py + :lines: 51-57 + :linenos: + +If the calculation is used in a standard multi, it will return `[1]`. +If the calculation is used in a follower, it will return `1`. + +An option could be a follower: + +.. literalinclude:: src/calculation.py + :lines: 59-65 + :linenos: + +If the calculation is used in a standard multi, it will return `[2]`. +If the calculation is used in a follower, it will return `2`. + +If the calculation is used in a follower we can also retrieve the actual follower index: + +.. literalinclude:: src/calculation.py + :lines: 67-73 + :linenos: + +Context has an argument +========================================= + +It is possible to recover a copy of the context directly in a function. On the other hand, the use of the context in a function is a slow action which will haunt the performances. Use it only in case of necessity: + +.. literalinclude:: src/calculation.py + :lines: 42-44 + :linenos: diff --git a/docs/cmdline_parser.rst b/docs/cmdline_parser.rst new file mode 100644 index 0000000..9a7ea76 --- /dev/null +++ b/docs/cmdline_parser.rst @@ -0,0 +1,718 @@ +.. .. default-role:: code +.. +.. ========================== +.. Tiramisu-cmdline-parser +.. ========================== +.. +.. +.. This tutorial is intended to be a gentle introduction to **Tiramisu +.. command-line parser**, a command-line parsing module that comes included with +.. the **Tiramisu**'s library. +.. +.. .. note:: There are a lot of other modules that fulfill the same task, +.. namely getopt (an equivalent for getopt() from the C language) and +.. argparse, from the python standard library. +.. +.. `tiramisu-cmdline-parser` enables us to *validate* the command line, +.. wich is a quite different scope -- much more powerfull. It is a +.. superset of the argparse_ module +.. +.. .. _argparse: https://docs.python.org/3/howto/argparse.html +.. +.. What is Tiramisu-cmdline-parser ? +.. ================================== +.. +.. Tiramisu-cmdline-parser is a free project that turns Tiramisu's Config into a command line interface. +.. +.. It automatically generates arguments, help and usage messages. Tiramisu (or +.. Tiramisu-API) validates all arguments provided by the command line's user. +.. +.. Tiramisu-cmdline-parser uses the well known argparse_ module and adds +.. functionnalities upon it. +.. +.. +.. Installation +.. ============== +.. +.. The best way is to use the python pip_ installer +.. +.. .. _pip: https://pip.pypa.io/en/stable/installing/ +.. +.. And then type: +.. +.. .. code-block:: bash +.. +.. pip install tiramisu-cmdline-parser +.. +.. Build a Tiramisu-cmdline-parser +.. ================================= +.. +.. Let’s show the sort of functionality that we are going to explore in this +.. introductory tutorial. +.. +.. We are going to start with a simple example, like making a proxy's +.. configuration script. +.. +.. First we are going to build the corresponding `Tiramisu` config object: +.. +.. .. literalinclude:: src/proxy.py +.. :lines: 1-44 +.. :linenos: +.. :name: Proxy1 +.. +.. Then we invopque the command line parsing library by creating a commandline +.. parser, and we give the configuration's object to it: +.. +.. .. literalinclude:: src/proxy.py +.. :lines: 46-48 +.. :linenos: +.. :name: Proxy2 +.. +.. Finally pretty printing the configuration: +.. +.. .. literalinclude:: src/proxy.py +.. :lines: 50-51 +.. :linenos: +.. :name: Proxy3 +.. +.. Let's display the help: +.. +.. .. code-block:: bash +.. +.. $ python3 src/proxy.py -h +.. usage: proxy.py [-h] [--dns_over_https] [--no-dns_over_https] +.. {No proxy,Manual proxy configuration,Automatic proxy +.. configuration URL} +.. +.. positional arguments: +.. {No proxy,Manual proxy configuration,Automatic proxy configuration URL} +.. Proxy's config mode +.. +.. optional arguments: +.. -h, --help show this help message and exit +.. --dns_over_https Enable DNS over HTTPS +.. --no-dns_over_https +.. +.. Positional argument +.. ====================== +.. +.. First of all, we have to set the positional argument :option:`proxy_mode`. +.. +.. .. option:: proxy_mode +.. +.. As it's a `ChoiceOption`, you only have three choices: +.. +.. - No proxy +.. - Manual proxy configuration +.. - Automatic proxy configuration URL +.. +.. Set proxy_mode to `No proxy`: +.. +.. .. code-block:: bash +.. +.. $ python3 src/proxy.py "No proxy" +.. {'dns_over_https': False, +.. 'proxy_mode': 'No proxy'} +.. +.. Requirements +.. ================ +.. +.. Disabled options are not visible as arguments in the command line. +.. Those parameters appears or disappears following the context: +.. +.. .. code-block:: bash +.. +.. $ python3 src/proxy.py "No proxy" -h +.. usage: proxy.py "No proxy" [-h] [--dns_over_https] [--no-dns_over_https] +.. {No proxy,Manual proxy configuration,Automatic +.. proxy configuration URL} +.. +.. positional arguments: +.. {No proxy,Manual proxy configuration,Automatic proxy configuration URL} +.. Proxy's config mode +.. +.. optional arguments: +.. -h, --help show this help message and exit +.. --dns_over_https Enable DNS over HTTPS +.. --no-dns_over_https +.. +.. If proxy_mode is set to "Automatic proxy configuration URL", some new options are visible: +.. +.. .. code-block:: bash +.. +.. $ python3 src/proxy.py "Automatic proxy configuration URL" -h +.. usage: proxy.py "Automatic proxy configuration URL" [-h] -i AUTO_CONFIG_URL +.. [--no_proxy.no_proxy_network.no_proxy_network [NO_PROXY_NETWORK [NO_PROXY_NETWORK ...]]] +.. [--no_proxy.no_proxy_network.pop-no_proxy_network INDEX] +.. --no_proxy.no_proxy_network.no_proxy_netmask +.. INDEX NO_PROXY_NETMASK +.. [--no_proxy.no_proxy_domain [NO_PROXY_DOMAIN [NO_PROXY_DOMAIN ...]]] +.. [--dns_over_https] +.. [--no-dns_over_https] +.. {No proxy,Manual proxy +.. configuration,Automatic +.. proxy configuration URL} +.. +.. positional arguments: +.. {No proxy,Manual proxy configuration,Automatic proxy configuration URL} +.. Proxy's config mode +.. +.. optional arguments: +.. -h, --help show this help message and exit +.. --dns_over_https Enable DNS over HTTPS +.. --no-dns_over_https +.. +.. configuration.automatic_proxy: +.. Automatic proxy setting +.. +.. -i AUTO_CONFIG_URL, --configuration.automatic_proxy.auto_config_url AUTO_CONFIG_URL +.. Proxy's auto config URL +.. +.. no_proxy: +.. Disabled proxy +.. +.. --no_proxy.no_proxy_domain [NO_PROXY_DOMAIN [NO_PROXY_DOMAIN ...]] +.. Domain names for which proxy will be desactivated +.. +.. no_proxy.no_proxy_network: +.. Network for which proxy will be desactivated +.. +.. --no_proxy.no_proxy_network.no_proxy_network [NO_PROXY_NETWORK [NO_PROXY_NETWORK ...]] +.. Network addresses +.. --no_proxy.no_proxy_network.pop-no_proxy_network INDEX +.. --no_proxy.no_proxy_network.no_proxy_netmask INDEX NO_PROXY_NETMASK +.. Netmask addresses +.. +.. Arguments +.. =========== +.. +.. Each option creates an argument. To change the value of this option, just launch the application with the appropriate argument: +.. +.. .. code-block:: bash +.. +.. $ python3 src/proxy.py "Manual proxy configuration" \ +.. --configuration.manual_proxy.http_ip_address 192.168.1.1 +.. {'configuration.manual_proxy.http_ip_address': '192.168.1.1', +.. 'configuration.manual_proxy.http_port': '8080', +.. 'configuration.manual_proxy.i': '192.168.1.1', +.. 'configuration.manual_proxy.p': '8080', +.. 'dns_over_https': False, +.. 'no_proxy.no_proxy_domain': [], +.. 'no_proxy.no_proxy_network.no_proxy_netmask': [], +.. 'no_proxy.no_proxy_network.no_proxy_network': [], +.. 'proxy_mode': 'Manual proxy configuration'} +.. +.. Fullpath argument or named argument +.. ===================================== +.. +.. By default, arguments are build with fullpath of option. +.. The `option http_ip_address` is in `manual_proxy` optiondescription, which is also in configuration optiondescription. +.. So the argument is :option:`--configuration.manual_proxy.http_ip_address`: +.. +.. .. code-block:: bash +.. +.. $ python3 src/proxy.py "Manual proxy configuration" \ +.. --configuration.manual_proxy.http_ip_address 192.168.1.1 +.. {'configuration.manual_proxy.http_ip_address': '192.168.1.1', +.. 'configuration.manual_proxy.http_port': '8080', +.. 'configuration.manual_proxy.i': '192.168.1.1', +.. 'configuration.manual_proxy.p': '8080', +.. 'dns_over_https': False, +.. 'no_proxy.no_proxy_domain': [], +.. 'no_proxy.no_proxy_network.no_proxy_netmask': [], +.. 'no_proxy.no_proxy_network.no_proxy_network': [], +.. 'proxy_mode': 'Manual proxy configuration'} +.. +.. If we set fullpath to `False`: +.. +.. .. code-block:: python +.. +.. parser = TiramisuCmdlineParser(proxy_config, fullpath=False) +.. +.. Arguments are build with the name of the option. +.. The option :option:`http_ip_address` is now :option`--http_ip_address`: +.. +.. .. code-block:: bash +.. +.. $ python3 src/proxy.py "Manual proxy configuration" \ +.. --http_ip_address 192.168.1.1 +.. {'configuration.manual_proxy.http_ip_address': '192.168.1.1', +.. 'configuration.manual_proxy.http_port': '8080', +.. 'configuration.manual_proxy.i': '192.168.1.1', +.. 'configuration.manual_proxy.p': '8080', +.. 'dns_over_https': False, +.. 'no_proxy.no_proxy_domain': [], +.. 'no_proxy.no_proxy_network.no_proxy_netmask': [], +.. 'no_proxy.no_proxy_network.no_proxy_network': [], +.. 'proxy_mode': 'Manual proxy configuration'} +.. +.. Short argument +.. =============== +.. +.. To have short argument, you just have to make `SymLinkOption` to this option: +.. +.. .. literalinclude:: src/proxy.py +.. :lines: 10-11 +.. :linenos: +.. :name: Proxy4 +.. +.. Now argument `-i` or `--configuration.manual_proxy.http_ip_address` can be used alternatively: +.. +.. .. code-block:: bash +.. +.. $ python3 src/proxy.py "Manual proxy configuration" \ +.. --configuration.manual_proxy.http_ip_address 192.168.1.1 +.. {'configuration.manual_proxy.http_ip_address': '192.168.1.1', +.. 'configuration.manual_proxy.http_port': '8080', +.. 'configuration.manual_proxy.i': '192.168.1.1', +.. 'configuration.manual_proxy.p': '8080', +.. 'dns_over_https': False, +.. 'no_proxy.no_proxy_domain': [], +.. 'no_proxy.no_proxy_network.no_proxy_netmask': [], +.. 'no_proxy.no_proxy_network.no_proxy_network': [], +.. 'proxy_mode': 'Manual proxy configuration'} +.. +.. +.. $ python3 src/proxy.py "Manual proxy configuration" \ +.. -i 192.168.1.1 +.. {'configuration.manual_proxy.http_ip_address': '192.168.1.1', +.. 'configuration.manual_proxy.http_port': '8080', +.. 'configuration.manual_proxy.i': '192.168.1.1', +.. 'configuration.manual_proxy.p': '8080', +.. 'dns_over_https': False, +.. 'no_proxy.no_proxy_domain': [], +.. 'no_proxy.no_proxy_network.no_proxy_netmask': [], +.. 'no_proxy.no_proxy_network.no_proxy_network': [], +.. 'proxy_mode': 'Manual proxy configuration'} +.. +.. Be carefull, short argument have to be uniqe in the whole configuration. +.. +.. Here `-i` argument is define a second time in same Config: +.. +.. .. literalinclude:: src/proxy.py +.. :lines: 17-18 +.. :linenos: +.. :name: Proxy5 +.. +.. But `http_ip_address` and `auto_config_url` are not accessible together: +.. +.. - `http_ip_address` is visible only if `proxy_mode` is "Manual proxy configuration" +.. - `auto_config_url` is only visible when `proxy_mode` is "Automatic proxy configuration URL" +.. +.. Boolean argument +.. =================== +.. +.. Boolean option creates two arguments: +.. +.. - --: it activates (set to True) the option +.. - --no-: it deactivates (set to False) the option +.. +.. .. code-block:: bash +.. +.. $ python3 src/proxy.py "No proxy" \ +.. --dns_over_https +.. {'dns_over_https': True, +.. 'proxy_mode': 'No proxy'} +.. +.. $ python3 src/proxy.py "No proxy" \ +.. --no-dns_over_https +.. {'dns_over_https': False, +.. 'proxy_mode': 'No proxy'} +.. +.. Multi +.. ========= +.. +.. Some values are multi. So we can set several value for this option. +.. +.. For example, we can set serveral domain (cadoles.com and gnu.org) to "Domain names for which proxy will be desactivated" option: +.. +.. .. code-block:: bash +.. +.. $ python3 src/proxy.py "Automatic proxy configuration URL" \ +.. --configuration.automatic_proxy.auto_config_url http://proxy.cadoles.com/proxy.pac \ +.. --no_proxy.no_proxy_domain cadoles.com gnu.org +.. {'configuration.automatic_proxy.auto_config_url': 'http://proxy.cadoles.com/proxy.pac', +.. 'configuration.automatic_proxy.i': 'http://proxy.cadoles.com/proxy.pac', +.. 'dns_over_https': False, +.. 'no_proxy.no_proxy_domain': ['cadoles.com', 'gnu.org'], +.. 'no_proxy.no_proxy_network.no_proxy_netmask': [], +.. 'no_proxy.no_proxy_network.no_proxy_network': [], +.. 'proxy_mode': 'Automatic proxy configuration URL'} +.. +.. Leadership +.. ============ +.. +.. Leadership option are also supported. The leader option is a standard multi option. +.. But follower option are not view as a multi option. Follower value are separate and we need to set index to set a follower option. +.. +.. If we want to had two "Network for which proxy will be desactivated": +.. +.. - 192.168.1.1/255.255.255.255 +.. - 192.168.0.0/255.255.255.0 +.. +.. We have to do: +.. +.. .. code-block:: bash +.. +.. $ python3 src/proxy.py "Automatic proxy configuration URL" \ +.. --configuration.automatic_proxy.auto_config_url http://proxy.cadoles.com/proxy.pac \ +.. --no_proxy.no_proxy_network.no_proxy_network 192.168.1.1 192.168.0.0 \ +.. --no_proxy.no_proxy_network.no_proxy_netmask 0 255.255.255.255 \ +.. --no_proxy.no_proxy_network.no_proxy_netmask 1 255.255.255.0 +.. {'configuration.automatic_proxy.auto_config_url': 'http://proxy.cadoles.com/proxy.pac', +.. 'configuration.automatic_proxy.i': 'http://proxy.cadoles.com/proxy.pac', +.. 'dns_over_https': False, +.. 'no_proxy.no_proxy_domain': [], +.. 'no_proxy.no_proxy_network.no_proxy_netmask': ['255.255.255.255', +.. '255.255.255.0'], +.. 'no_proxy.no_proxy_network.no_proxy_network': ['192.168.1.1', '192.168.0.0'], +.. 'proxy_mode': 'Automatic proxy configuration URL'} +.. +.. We cannot reduce leader lenght: +.. +.. .. code-block:: bash +.. +.. $ python3 src/proxy.py "Automatic proxy configuration URL" \ +.. --configuration.automatic_proxy.auto_config_url http://proxy.cadoles.com/proxy.pac \ +.. --no_proxy.no_proxy_network.no_proxy_network 192.168.1.1 192.168.0.0 \ +.. --no_proxy.no_proxy_network.no_proxy_netmask 0 255.255.255.255 \ +.. --no_proxy.no_proxy_network.no_proxy_netmask 1 255.255.255.0 \ +.. --no_proxy.no_proxy_network.no_proxy_network 192.168.1.1 +.. usage: proxy.py -i "http://proxy.cadoles.com/proxy.pac" --no_proxy.no_proxy_network.no_proxy_network "192.168.1.1" "192.168.0.0" "Automatic proxy configuration URL" +.. [-h] -i AUTO_CONFIG_URL +.. [--no_proxy.no_proxy_network.no_proxy_network [NO_PROXY_NETWORK [NO_PROXY_NETWORK ...]]] +.. [--no_proxy.no_proxy_network.pop-no_proxy_network INDEX] +.. --no_proxy.no_proxy_network.no_proxy_netmask INDEX NO_PROXY_NETMASK +.. [--no_proxy.no_proxy_domain [NO_PROXY_DOMAIN [NO_PROXY_DOMAIN ...]]] +.. [--dns_over_https] [--no-dns_over_https] +.. {No proxy,Manual proxy configuration,Automatic proxy configuration URL} +.. proxy.py: error: cannot reduce length of the leader "--no_proxy.no_proxy_network.no_proxy_network" +.. +.. So an argument --pop- is automatically created. You need to specified index as parameter: +.. +.. .. code-block:: bash +.. +.. $ python3 src/proxy.py "Automatic proxy configuration URL" \ +.. --configuration.automatic_proxy.auto_config_url http://proxy.cadoles.com/proxy.pac \ +.. --no_proxy.no_proxy_network.no_proxy_network 192.168.1.1 192.168.0.0 \ +.. --no_proxy.no_proxy_network.no_proxy_netmask 0 255.255.255.255 \ +.. --no_proxy.no_proxy_network.pop-no_proxy_network 1 +.. {'configuration.automatic_proxy.auto_config_url': 'http://proxy.cadoles.com/proxy.pac', +.. 'configuration.automatic_proxy.i': 'http://proxy.cadoles.com/proxy.pac', +.. 'dns_over_https': False, +.. 'no_proxy.no_proxy_domain': [], +.. 'no_proxy.no_proxy_network.no_proxy_netmask': ['255.255.255.255'], +.. 'no_proxy.no_proxy_network.no_proxy_network': ['192.168.1.1'], +.. 'proxy_mode': 'Automatic proxy configuration URL'} +.. +.. Validation +.. =============== +.. +.. All arguments are validated successively by argparser and Tiramisu: +.. +.. .. code-block:: bash +.. +.. $ python3 src/proxy.py "Automatic proxy configuration URL" \ +.. --configuration.automatic_proxy.auto_config_url cadoles.com +.. usage: proxy.py "Automatic proxy configuration URL" [-h] -i AUTO_CONFIG_URL +.. [--no_proxy.no_proxy_network.no_proxy_network [NO_PROXY_NETWORK [NO_PROXY_NETWORK ...]]] +.. [--no_proxy.no_proxy_network.pop-no_proxy_network INDEX] +.. --no_proxy.no_proxy_network.no_proxy_netmask +.. INDEX NO_PROXY_NETMASK +.. [--no_proxy.no_proxy_domain [NO_PROXY_DOMAIN [NO_PROXY_DOMAIN ...]]] +.. [--dns_over_https] +.. [--no-dns_over_https] +.. {No proxy,Manual proxy +.. configuration,Automatic +.. proxy configuration URL} +.. proxy.py: error: "cadoles.com" is an invalid URL for "Proxy’s auto config URL", must start with http:// or https:// +.. +.. In error message, we have the option description ("Proxy's auto config URL") by default. +.. +.. That why we redefined display_name function: +.. +.. .. literalinclude:: src/proxy.py +.. :lines: 40-43 +.. :linenos: +.. :name: Proxy6 +.. +.. Now we have -- as description: +.. +.. .. code-block:: bash +.. +.. $ python3 src/proxy.py "Automatic proxy configuration URL" \ +.. --configuration.automatic_proxy.auto_config_url cadoles.com +.. usage: proxy.py "Automatic proxy configuration URL" [-h] -i AUTO_CONFIG_URL +.. [--no_proxy.no_proxy_network.no_proxy_network [NO_PROXY_NETWORK [NO_PROXY_NETWORK ...]]] +.. [--no_proxy.no_proxy_network.pop-no_proxy_network INDEX] +.. --no_proxy.no_proxy_network.no_proxy_netmask +.. INDEX NO_PROXY_NETMASK +.. [--no_proxy.no_proxy_domain [NO_PROXY_DOMAIN [NO_PROXY_DOMAIN ...]]] +.. [--dns_over_https] +.. [--no-dns_over_https] +.. {No proxy,Manual proxy +.. configuration,Automatic +.. proxy configuration URL} +.. proxy.py: error: "cadoles.com" is an invalid URL for "--configuration.automatic_proxy.auto_config_url", must start with http:// or https:// +.. +.. Mandatory +.. ============= +.. +.. Obviously the mandatory options are checked. +.. +.. The positional argument is mandatory, so if we don't set it, an error occured: +.. +.. .. code-block:: bash +.. +.. $ python3 src/proxy.py +.. usage: proxy.py [-h] [--dns_over_https] [--no-dns_over_https] +.. {No proxy,Manual proxy configuration,Automatic proxy +.. configuration URL} +.. proxy.py: error: the following arguments are required: proxy_mode +.. +.. Others arguments are also check: +.. +.. .. code-block:: bash +.. +.. $ python3 src/proxy.py "Automatic proxy configuration URL" +.. usage: proxy.py "Automatic proxy configuration URL" [-h] -i AUTO_CONFIG_URL +.. [--no_proxy.no_proxy_network.no_proxy_network [NO_PROXY_NETWORK [NO_PROXY_NETWORK ...]]] +.. [--no_proxy.no_proxy_network.pop-no_proxy_network INDEX] +.. --no_proxy.no_proxy_network.no_proxy_netmask +.. INDEX NO_PROXY_NETMASK +.. [--no_proxy.no_proxy_domain [NO_PROXY_DOMAIN [NO_PROXY_DOMAIN ...]]] +.. [--dns_over_https] +.. [--no-dns_over_https] +.. {No proxy,Manual proxy +.. configuration,Automatic +.. proxy configuration URL} +.. proxy.py: error: the following arguments are required: --configuration.automatic_proxy.auto_config_url +.. +.. Persistence configuration and mandatories validation +.. ====================================================== +.. +.. First of all, activate persistence configuration and remove mandatory validation: +.. +.. .. literalinclude:: src/proxy_persistent.py +.. :lines: 43-46 +.. :linenos: +.. :name: Proxy7 +.. +.. We can disabled mandatory validation in parse_args function. +.. +.. .. literalinclude:: src/proxy_persistent.py +.. :lines: 51 +.. :linenos: +.. :name: Proxy8 +.. +.. In this case, we can store incomplete value: +.. +.. .. code-block:: bash +.. +.. $ python3 src/proxy_persistent.py 'Manual proxy configuration' +.. {'configuration.manual_proxy.http_ip_address': None, +.. 'configuration.manual_proxy.http_port': '8080', +.. 'configuration.manual_proxy.i': None, +.. 'configuration.manual_proxy.p': '8080', +.. 'dns_over_https': False, +.. 'no_proxy.no_proxy_domain': [], +.. 'no_proxy.no_proxy_network.no_proxy_netmask': [], +.. 'no_proxy.no_proxy_network.no_proxy_network': [], +.. 'proxy_mode': 'Manual proxy configuration'} +.. +.. We can complete configuration after: +.. +.. .. code-block:: bash +.. +.. $ python3 src/proxy_persistent.py -i 192.168.1.1 +.. {'configuration.manual_proxy.http_ip_address': '192.168.1.1', +.. 'configuration.manual_proxy.http_port': '8080', +.. 'configuration.manual_proxy.i': '192.168.1.1', +.. 'configuration.manual_proxy.p': '8080', +.. 'dns_over_https': False, +.. 'no_proxy.no_proxy_domain': [], +.. 'no_proxy.no_proxy_network.no_proxy_netmask': [], +.. 'no_proxy.no_proxy_network.no_proxy_network': [], +.. 'proxy_mode': 'Manual proxy configuration'} +.. +.. When configuration is already set, help command, display already set options is usage ligne. +.. +.. .. code-block:: bash +.. +.. $ python3 src/proxy_persistent.py -h +.. usage: proxy_persistent.py -i "192.168.1.1" "Manual proxy configuration" +.. [..] +.. +.. Description and epilog +.. ============================ +.. +.. As argparser, description and epilog message can be added to the generated help: +.. +.. .. code-block:: python +.. +.. parser = TiramisuCmdlineParser(proxy_config, description='New description!', epilog='New epilog!') +.. +.. .. code-block:: bash +.. +.. $ python3 src/proxy.py -h +.. usage: proxy.py [-h] [--dns_over_https] [--no-dns_over_https] +.. {No proxy,Manual proxy configuration,Automatic proxy +.. configuration URL} +.. +.. New description! +.. +.. positional arguments: +.. {No proxy,Manual proxy configuration,Automatic proxy configuration URL} +.. Proxy's config mode +.. +.. optional arguments: +.. -h, --help show this help message and exit +.. --dns_over_https Enable DNS over HTTPS +.. --no-dns_over_https +.. +.. New epilog! +.. +.. +.. By default, TiramisuCmdlineParser objects line-wrap the description and epilog texts in command-line help messages. +.. +.. If there are line breaks in description or epilog, it automatically replace by a space. You need to change formatter class: +.. +.. .. code-block:: python +.. +.. from argparse import RawDescriptionHelpFormatter +.. parser = TiramisuCmdlineParser(proxy_config, description='New description!\nLine breaks', epilog='New epilog!\nLine breaks', formatter_class=RawDescriptionHelpFormatter) +.. +.. .. code-block:: bash +.. +.. $ python3 src/proxy.py -h +.. usage: proxy.py [-h] [--dns_over_https] [--no-dns_over_https] +.. {No proxy,Manual proxy configuration,Automatic proxy +.. configuration URL} +.. +.. New description! +.. Line breaks +.. +.. positional arguments: +.. {No proxy,Manual proxy configuration,Automatic proxy configuration URL} +.. Proxy's config mode +.. +.. optional arguments: +.. -h, --help show this help message and exit +.. --dns_over_https Enable DNS over HTTPS +.. --no-dns_over_https +.. +.. New epilog! +.. Line breaks +.. +.. Hide empty optiondescription +.. =============================== +.. +.. An empty optiondescription, is an optiondescription without any option (could have others optiondescriptions). +.. +.. For example, configuration is an empty optiondescription: +.. +.. .. literalinclude:: src/proxy.py +.. :lines: 23-24 +.. :linenos: +.. :name: Proxy9 +.. +.. This optiondescription doesn't appears in help: +.. +.. .. code-block:: bash +.. +.. $ python3 src/proxy.py "No proxy" -h +.. usage: proxy.py "No proxy" [-h] [--dns_over_https] [--no-dns_over_https] +.. {No proxy,Manual proxy configuration,Automatic +.. proxy configuration URL} +.. +.. positional arguments: +.. {No proxy,Manual proxy configuration,Automatic proxy configuration URL} +.. Proxy's config mode +.. +.. optional arguments: +.. -h, --help show this help message and exit +.. --dns_over_https Enable DNS over HTTPS +.. --no-dns_over_https +.. +.. +.. +.. This behavior is, in fact, due to two conditions: +.. +.. - there is no option +.. - there is no description (None) +.. +.. If we add description: +.. +.. .. code-block:: python +.. +.. configuration = OptionDescription('configuration', 'Configuration', +.. [manual_proxy, automatic_proxy]) +.. +.. This optiondescription is specified in help: +.. +.. .. code-block:: bash +.. +.. $ python3 src/proxy.py "No proxy" -h +.. usage: proxy.py "No proxy" [-h] [--dns_over_https] [--no-dns_over_https] +.. {No proxy,Manual proxy configuration,Automatic +.. proxy configuration URL} +.. +.. positional arguments: +.. {No proxy,Manual proxy configuration,Automatic proxy configuration URL} +.. Proxy's config mode +.. +.. optional arguments: +.. -h, --help show this help message and exit +.. --dns_over_https Enable DNS over HTTPS +.. --no-dns_over_https +.. +.. configuration: +.. Configuration +.. +.. If you don't want empty optiondescription even if there is a description, you could add remove_empty_od to True in parse_args function: +.. +.. .. code-block:: python +.. +.. parser = TiramisuCmdlineParser(proxy_config, remove_empty_od=True) +.. +.. SubConfig +.. ================ +.. +.. Entire Config is transformed into an argument by default. +.. +.. It could be interesting to display only 'configuration' OptionDescription. +.. +.. To do this, we have to define default all mandatories options outside this scope: +.. +.. .. code-block:: python +.. +.. proxy_mode = ChoiceOption('proxy_mode', 'Proxy\'s config mode', ('No proxy', +.. 'Manual proxy configuration', +.. 'Automatic proxy configuration URL'), +.. default='Manual proxy configuration', +.. properties=('positional', 'mandatory')) +.. +.. Finally specified the root argument to `TiramisuCmdlineParser`: +.. +.. .. code-block:: python +.. +.. parser = TiramisuCmdlineParser(proxy_config, root='configuration') +.. +.. Now, only sub option of configuration is proposed: +.. +.. .. code-block:: bash +.. +.. $ python3 src/proxy.py -h +.. usage: proxy.py [-h] -i HTTP_IP_ADDRESS -p [HTTP_PORT] +.. +.. optional arguments: +.. -h, --help show this help message and exit +.. +.. configuration.manual_proxy: +.. Manual proxy settings +.. +.. -i HTTP_IP_ADDRESS, --configuration.manual_proxy.http_ip_address HTTP_IP_ADDRESS +.. Proxy's HTTP IP +.. -p [HTTP_PORT], --configuration.manual_proxy.http_port [HTTP_PORT] +.. Proxy's HTTP Port +.. diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..e105d3b --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,149 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file does only contain a selection of the most common options. For a +# full list see the documentation: +# http://www.sphinx-doc.org/en/master/config + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- Project information ----------------------------------------------------- + +project = 'Tiramisu' +copyright = '2011-2023, Silique' +author = 'egarette' + +# The short X.Y version +version = '' +# The full version, including alpha/beta/rc tags +release = '4.0' + + +# -- General configuration --------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. + +extensions = [ + 'sphinx.ext.extlinks', 'sphinx_lesson', + #'myst_parser', 'sphinx.ext.extlinks' +] +# +#myst_enable_extensions = [ +# "amsmath", +# "attrs_inline", +# "colon_fence", +# "deflist", +# "dollarmath", +# "fieldlist", +# "html_admonition", +# "html_image", +## "linkify", +# "replacements", +# "smartquotes", +# "strikethrough", +# "substitution", +# "tasklist", +#] + + +# **extlinks** 'sphinx.ext.extlinks', +# enables syntax like :proxy:`my source ` in the src files +extlinks = {'proxy': ('/proxy/%s.html', + 'external link: ')} + +default_role = "code" + +html_theme = "sphinx_rtd_theme" + +pygments_style = 'sphinx' + +html_short_title = "Tiramisu" +html_title = "Tiramisu documenation" + +# If true, links to the reST sources are added to the pages. +html_show_sourcelink = False + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +html_show_sphinx = False + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +html_show_copyright = True + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +#source_suffix = ['.rst', '.md'] +source_suffix = '.rst' +#source_suffix = { +# '.rst': 'restructuredtext', +# '.txt': 'restructuredtext', +# '.md': 'markdown', +#} +# The master toctree document. +master_doc = 'index' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = 'en' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = None + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +#html_theme = 'alabaster' +# **themes** +#html_theme = 'bizstyle' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# The default sidebars (for documents that don't match any pattern) are +# defined by theme itself. Builtin themes are using these templates by +# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', +# 'searchbox.html']``. +# +# html_sidebars = {} + +def setup(app): + app.add_css_file('css/custom.css') diff --git a/doc/config.png b/docs/config.png similarity index 100% rename from doc/config.png rename to docs/config.png diff --git a/docs/config.rst b/docs/config.rst new file mode 100644 index 0000000..75234f5 --- /dev/null +++ b/docs/config.rst @@ -0,0 +1,138 @@ +The :class:`Config` +==================== + +Tiramisu is made of almost three main classes/concepts : + +- the :class:`Option` stands for the option types +- the :class:`OptionDescription` is the schema, the option's structure +- the :class:`Config` which is the whole configuration entry point + +.. image:: config.png + +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). + +.. toctree:: + :maxdepth: 2 + + option + options + symlinkoption + own_option + +Option description are nested Options +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The :class:`Option` (in this case the :class:`BoolOption`), +are organized into a tree into nested +:class:`~tiramisu.option.OptionDescription` objects. + +Every option has a name, as does every option group. + +.. toctree:: + :maxdepth: 2 + + optiondescription + dynoptiondescription + leadership + + +Config +~~~~~~ + +Let's perform a *Getting started* code review : + +.. literalinclude:: src/getting_started.py + :lines: 1-12 + :linenos: + :name: GettingStarted + +Let's review the code. First, line 7, we create an :class:`OptionDescription` named `optgroup`. + +.. literalinclude:: src/getting_started.py + :lines: 4, 6-7 + :emphasize-lines: 3 + +Option objects can be created in different ways, here we create a +:class:`BoolOption` + +.. literalinclude:: src/getting_started.py + :lines: 4, 8-9 + :emphasize-lines: 3 + +Then, line 12, we make a :class:`Config` with the :class:`OptionDescription` we +built : + +.. literalinclude:: src/getting_started.py + :lines: 3, 12 + :emphasize-lines: 2 + +Here is how to print our :class:`Config` details: + +.. literalinclude:: src/getting_started.py + :lines: 15 + +.. code-block:: bash + + 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 + nowarnings Do not warnings during validation + + 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 + value Manage config value + +Then let's print our :class:`Option` details. + +.. literalinclude:: src/getting_started.py + :lines: 17 + +.. code-block:: bash + + 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) + name get the name + updates Updates value with tiramisu format + +Finaly, let's print the :class:`Config`. + +.. literalinclude:: src/getting_started.py + :lines: 19 + +.. code-block:: bash + + + +:download:`download the getting started code ` + +Go futher with `Option` and `Config` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. toctree:: + :maxdepth: 2 + + property + validator + calculation diff --git a/docs/custom/static/custom.css b/docs/custom/static/custom.css new file mode 100644 index 0000000..1d06807 --- /dev/null +++ b/docs/custom/static/custom.css @@ -0,0 +1,39 @@ +@import url("bizstyle.css"); +/* a button for the download links */ +a.download{ + margin-top:15px; + max-width:190px; + background-color:#eee; + border-color:#888888; + color:#333; + display:inline-block; + vertical-align:middle; + text-align:center; + text-decoration:none; + align-items:flex-start; + cursor:default; + -webkit-appearence: push-button; + border-style: solid; + border-width: 1px; + border-radius: 5px; + font-size: 1em; + font-family: inherit; + border-color: #000; + padding-left: 5px; + padding-right: 5px; + width: 100%; + min-height: 30px; +} + +/* the bash output looks like different */ +div.highlight-bash { + + background-color:#eee; + border-style: solid; + border-width: 1px; + border-radius: 5px; + border-color: #000; + padding-left: 5px; + padding-right: 5px; + } + diff --git a/docs/custom/theme.conf b/docs/custom/theme.conf new file mode 100644 index 0000000..8dc4c6a --- /dev/null +++ b/docs/custom/theme.conf @@ -0,0 +1,4 @@ +[theme] +inherit = bizstyle +stylesheet = custom.css + diff --git a/docs/dynoptiondescription.rst b/docs/dynoptiondescription.rst new file mode 100644 index 0000000..a9bc6dc --- /dev/null +++ b/docs/dynoptiondescription.rst @@ -0,0 +1,59 @@ +========================================================== +Dynamic option description: :class:`DynOptionDescription` +========================================================== + +Dynamic option description +============================================== + +Dynamic option description is an :class:`OptionDescription` which multiplies according to the return of a function. + +.. list-table:: + :widths: 15 45 + :header-rows: 1 + + * - Parameter + - Comments + + * - name + - The `name` is important to retrieve this option. + + * - doc + - The `description` allows the user to understand where this option will be used for. + + * - children + - The list of children (Option) include inside. + + Note:: the option can be an :doc:`option` or an other option description + + * - suffixes + - Suffixes is a :doc:`calculation` that return the list of suffixes used to create dynamic option description. + + * - properties + - A list of :doc:`property` (inside a frozenset(). + +Example +============== + +Let's try: + +>>> 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 diff --git a/doc/gettingstarted.md b/docs/gettingstarted.rst similarity index 61% rename from doc/gettingstarted.md rename to docs/gettingstarted.rst index a0e0898..7e59507 100644 --- a/doc/gettingstarted.md +++ b/docs/gettingstarted.rst @@ -1,6 +1,9 @@ -# Getting started +================================== +Getting started +================================== -## What is options handling ? +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 @@ -9,7 +12,8 @@ options. To circumvent these problems the configuration control was introduced. -## What is Tiramisu ? +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 @@ -19,24 +23,35 @@ 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 +Installation +------------- -The best way is to use the python [pip](https://pip.pypa.io/en/stable/installing/) installer +The best way is to use the python pip_ installer + +.. _pip: https://pip.pypa.io/en/stable/installing/ And then type: -```bash -$ pip install tiramisu -``` +.. code-block:: bash -### Advanced users + pip install tiramisu + +Advanced users +============== + +.. _gettingtiramisu: + +- the library's development homepage is there_ + +.. _there: https://forge.cloud.silique.fr/stove/tiramisu/ 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 -``` +.. code-block:: bash + + git clone https://forge.cloud.silique.fr/stove/tiramisu.git This will get you a fresh checkout of the code repository in a local directory -named "tiramisu". +named ``tiramisu``. + diff --git a/docs/glossary.rst b/docs/glossary.rst new file mode 100644 index 0000000..a4685aa --- /dev/null +++ b/docs/glossary.rst @@ -0,0 +1,84 @@ +.. default-role:: literal + +Glossary +========== + +.. glossary:: + + configuration + + Global configuration object, wich contains the whole configuration + options *and* their descriptions (option types and group) + + schema + option description + + see :class:`tiramisu.option.OptionDescription` + + The schema of a configuration : + + - the option types + + - how they are organised in groups or even subgroups, that's why we + call them **groups** too. + + configuration option + + An option object wich has a name and a value and can be accessed + from the configuration object + + access rules + + Global access rules are : :meth:`~config.CommonConfig.read_write()` or + :meth:`~config.Config.read_only()` + + default value + + Default value of a configuration option. The default value can be + set at instanciation time, or even at any moment. Remember that if + you reset the default value, the owner reset to `default` + + freeze + + A whole configuration can be frozen (used in read only access). + + A single option can be frozen too. + + value owner + + When an option is modified, including at the instanciation, we + always know who has modified it. It's the owner of the option. + + properties + + an option with properties is a option wich has property like 'hidden' or 'disabled' is an option + wich has restricted acces rules. + + hidden option + + a hidden option has a different behaviour on regards to the access + of the value in the configuration. + + disabled option + + a disabled option has a different behaviour on regards to the access + of the value in the configuration. + + mandatory option + + A mandatory option is a configuration option wich value has to be + set, that is the default value cannot be `None`. + + consistency + + Preserving the consistency in a whole configuration is a tricky thing, + tiramisu takes care of it for you. + + context + + The context is a :class:`tiramisu.setting.Setting()` object in the + configuration that enables us to access to the global properties + + for example the `read_write` or `read_only` :term:`access rules` + + diff --git a/docs/images/firefox_preferences.png b/docs/images/firefox_preferences.png new file mode 100644 index 0000000000000000000000000000000000000000..4a6fcc21f95128d07b4abfc90216ffc74cc7e963 GIT binary patch literal 83071 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 !O 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 h@0|Hm invalide pour des propriétés pour protocols, doit être un frozenset" diff --git a/docs/leadership.rst b/docs/leadership.rst new file mode 100644 index 0000000..f70afee --- /dev/null +++ b/docs/leadership.rst @@ -0,0 +1,48 @@ +========================================================== +Leadership OptionDescription: :class:`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 +============================================== + +.. list-table:: + :widths: 15 45 + :header-rows: 1 + + * - Parameter + - Comments + + * - name + - The `name` is important to retrieve this option. + + * - doc + - The `description` allows the user to understand where this option will be used for. + + * - children + - The list of children (Option) include inside. + + Note:: the option has to be multi or submulti option and not other option description. + + * - properties + - A list of :doc:`property` (inside a frozenset(). + +Example +==================== + +Let's try: + +>>> from tiramisu import StrOption, Leadership +>>> users = StrOption('users', 'User', multi=True) +>>> passwords = StrOption('passwords', 'Password', multi=True) +>>> Leadership('users', +... 'User allow to connect', +... [users, passwods]) diff --git a/docs/logo.png b/docs/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..084a5346d00ddbdad245dd3706d8128ebf47fdf0 GIT binary patch literal 2591 zcmV+)3gGpLP)e zSad^gZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{011alL_t(|+U=ctj8s=0$3J&o zECai+ypfRQ)j}y$upuFkfNe0Ljg3|<7^980O{r7^1~rYTkEB)qL5x@p#ZqFdMJ>V9 zS|6=ITPs>x14LR{S`d&HT40w2cA1^m@sE3EXLfd$-I<-c+`aStBsXX7y>sW@d(L-$ z=lsqSgb+dqA%qY@2qA&m$9E?~=b-`$aP94A)mx--TY#|b&1*pTe;Cmu>%d}Gg0`zIdd*Jlo0#e46w zqFYv`=tQyga2y8=*<>O#dBr#46K=UD0W<=?k?^`_mGgjYz)nf0x{O}*$)WM4*&V=2 zph3dR^L-nBPco@K*>uv3S+c?@D3(Zgmc=$aA(?oS^~RfK=XPgHc-=`!CLSehBo^|C z1L;fQiCX<*C<+E2#+WPa4+bJWe5P&I8v{5Ed`MF3%Gtkf=gh7rJNK{s`c}fjM&Jyh zx^%x;yj8$w3(6)Ffv0;cMB~`61$FCA`eJhz(E+^k$X5u(>SXcy!7aFLj)k%Q(Vo71 z=;1q}1h`@Rnsr-<#2dOOu=%yWcCS83w2^K-OE_LH;Z?}tBGDP#cE?iQ@CB1e2UT-n zU|6X?RlMo?(g|7L$Y6>OzX|V~z@5PO0?T(tiSPZ&Zb9V+;6SMp!)MH+ZsK$jr;cq|u8Gi)p%`NZm2=@o z;bnlM@?CpSq!m|Wcq5VFjh$5~&LHa@rh2@92yw6>;fb0Oo_Y;?kCE`YbJ~WZr4MgN zHoun>UWon%tQ{M8tfNwg==zVuXXAk~W! zURQD*0safbB$;ZI=^V0&OUbu4+#RxM`zgOZ?A5Htdk$C){2X_**)zaq+&a8kcRr>7 zB+tE<%(P$Vldsls_M~aq`{z*G(F0K{x|P6Q{U3W~4saX|#;(@(A7HxQ6WvdirO8H^ zvRyY_B9u2YpaZv)#k-0=A%qY@2vO_K#nomxu);byP6hFJ4tSn?tBjEFuE7Z z>BFXfSdk;)c}J$>GSlzhNNn^ZoN$y#!x*OB_hX!JRFNa+!rL-lINrcDYd7IUVwg;d zY$`br(?*1YZWtuIK8Vj`(w$!H0L@jh+KTVr5}p`H9#a`l49Z#6W$}WL-G(Ea-?tOW zs5DNsp6PUs`*_Jj;MRiuEN+|aVW3&Ut3}s88sqezHk0kRT*>8qouXr=&0>ou9|xEV z98^|YyKJZLYX!`-*IX#`9u6=Kw=Z8#={r=$Q_-Za4C}{zPT+PR-CJgMt*ln`i)FYA z9p_fL{JexGJa?jjzZ8k^Wo5O(b0->z18eO5-*6{3lJJD*q=1dMBjCWD0ZPIX1IFBf z&+{Za;Z@l#g&lHfN#m~NmIC(3xe#9PBBiTMt_|YL%HjzLPe^#;>M`Gz^D?&FH$=h{ z1Hm)6b$Pk8nI++gfncHye__MlOL)R-?kz}@du>)b6&NAm2`~An72ExG|Dp|Vm+*vF zI6%~fuL3ua?}xWFXfLY@WdHD(6`i5$Qc$@b$rH#gw>7Mdw~BICrbET2W1=6@1=J*OrRK2)g9H51e*Ob)r_edLt2bsOJmD3S3qF5B&V?8#PT0`cZ4+=K?h>LB zp74~1$+*N&2oDSTj zoch9zxHCB2RE9u4ZG}3kkKf-;zQv-DTuVlik}Mc}U=y^?qu)PCd6mG63WPVu9@|=Q ztX@eL45rxx{f7s?owIhotDsC-+)itNFDto%#mhEfpReL?-eZ5Wxx(ug;4Y5*Ti`pu zlpsdD3bgP~U?Fe;*k#LF<+hWCfu+D_fC={cu>`!mRrH^mfpkIE)}QSGR534$S0)pw zyzkp(+yOgk(GWriA%qY@2qA>> from tiramisu import StrOption +>>> StrOption('welcome', +... 'Welcome message to the user login') + + +Add a default value: + +>>> from tiramisu import StrOption +>>> StrOption('welcome', +... 'Welcome message to the user login', +... 'Hey guys, welcome here!') + +Or a calculated default value: + +>>> from tiramisu import StrOption, Calculation +>>> def get_value(): +... return 'Hey guys, welcome here' +>>> StrOption('welcome', +... 'Welcome message to the user login', +... Calculation(get_value)) + + +A multi option. In this case, the default value has to be a list: + +>>> from tiramisu import StrOption +>>> 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: + +>>> from tiramisu import StrOption, 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 :doc:`calculation`. For a multi, the function have to return a list or have to be in a list: + +>>> from tiramisu import StrOption, Calculation +>>> def get_values(): +... return ['1 kilogram of carrots', 'leeks', '1 kilogram of potatos'] +>>> StrOption('shopping_list', +... 'The shopping list', +... Calculation(get_values), +... multi=True) + +or + +>>> from tiramisu import StrOption, Calculation +>>> 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) + +Add a default_multi: + +>>> from tiramisu import StrOption, submulti +>>> 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) + +Or calculated default_multi: + +>>> from tiramisu import StrOption +>>> 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) diff --git a/docs/optiondescription.rst b/docs/optiondescription.rst new file mode 100644 index 0000000..2fb6e82 --- /dev/null +++ b/docs/optiondescription.rst @@ -0,0 +1,38 @@ +============================================== +Generic container: :class:`OptionDescription` +============================================== + +Option description +=================================== + +.. list-table:: + :widths: 15 45 + :header-rows: 1 + + * - Parameter + - Comments + + * - name + - The `name` is important to retrieve this option. + + * - doc + - The `description` allows the user to understand where this option will be used for. + + * - children + - The list of children (Option) include inside. + .. note:: the option can be an :doc:`option` or an other option description + + * - properties + - A list of :doc:`property` (inside a frozenset(). + +Examples +============== + +>>> from tiramisu import StrOption, OptionDescription +>>> child1 = StrOption('first', 'First basic option') +>>> child2 = StrOption('second', 'Second basic option') +>>> child3 = StrOption('third', 'Third basic option') +>>> od1 = OptionDescription('od1', 'First option description', [child3]) +>>> OptionDescription('basic', +... 'Basic options', +... [child1, child2, od1]) diff --git a/docs/options.rst b/docs/options.rst new file mode 100644 index 0000000..24acf58 --- /dev/null +++ b/docs/options.rst @@ -0,0 +1,330 @@ +================================== +Default Options type +================================== + +Basic options +================================== + +Options +----------- + +.. list-table:: + :widths: 20 40 40 + :header-rows: 1 + + * - Type + - Comments + - Extra parameters + + * - StrOption + - Option that accept any textual data in Tiramisu. + - + + * - IntOption + - Option that accept any integers number in Tiramisu. + - + - min_number + - max_number + + * - FloatOption + - Option that accept any floating point number in Tiramisu. + - + + * - BoolOption + - Boolean values are the two constant objects False and True. + - + +Examples +------------ + +Textual option: + +>>> from tiramisu import StrOption +>>> StrOption('str', 'str', 'value') +>>> try: +... StrOption('str', 'str', 1) +... except ValueError as err: +... print(err) +... +"1" is an invalid string for "str" + +Integer option: + +>>> from tiramisu import IntOption +>>> IntOption('int', 'int', 1) +>>> IntOption('int', 'int', 10, min_number=10, max_number=15) +>>> try: +... IntOption('int', 'int', 16, max_number=15) +... except ValueError as err: +... print(err) +... +"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: + +>>> from tiramisu import FloatOption +>>> FloatOption('float', 'float', 10.1) + +Boolean option: + +>>> from tiramisu import BoolOption +>>> BoolOption('bool', 'bool', True) +>>> BoolOption('bool', 'bool', False) + +Network options +================================== + +.. list-table:: + :widths: 20 40 40 + :header-rows: 1 + + * - Type + - Comments + - Extra parameters + + * - 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. + - + - private_only: restrict to only private IPv4 address + - allow_reserved: allow the IETF reserved address + - cidr: Classless Inter-Domain Routing (CIDR) is a method for allocating IP addresses and IP routing, such as 192.168.0.1/24 + + * - NetworkOption + - IP networks may be divided into subnetworks + - + - cidr: Classless Inter-Domain Routing (CIDR) is a method for allocating IP addresses and IP routing, such as 192.168.0.0/24 + + * - NetmaskOption + - For IPv4, a network may also be characterized by its subnet mask or netmask. This option allow you to enter a netmask. + - + + * - BroadcastOption + - The last address within a network broadcast transmission to all hosts on the link. This option allow you to enter a broadcast: + - + + * - PortOption + - A port is a network communication endpoint. It's a string object + - + - allow_range: allow is a list of port where we specified first port and last port number with the separator is `:` + - allow_zero: allow the port 0 + - allow_wellknown: by default, the well-known ports (also known as system ports) those from 1 through 1023 are allowed, you can disabled it + - allow_registred: by default, the registered ports are those from 1024 through 49151 are allowed, you can disabled it + - allow_private: allow dynamic or private ports, which are those from 49152 through 65535, one common use for this range is for ephemeral ports + + +Examples +------------------------------------------- + +>>> from tiramisu import IPOption +>>> IPOption('ip', 'ip', '192.168.0.24') +>>> IPOption('ip', 'ip', '1.1.1.1') +>>> try: +... IPOption('ip', 'ip', '1.1.1.1', private_only=True) +... except ValueError as err: +... print(err) +... +"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. + +>>> from tiramisu import IPOption +>>> try: +... IPOption('ip', 'ip', '255.255.255.255') +... except ValueError as err: +... print(err) +... +"255.255.255.255" is an invalid IP for "ip", mustn't be reserved IP +>>> 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. + +>>> from tiramisu import IPOption +>>> 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) +... +"192.168.0.0/24" is an invalid IP for "ip", it's in fact a network address + +>>> from tiramisu import NetworkOption +>>> NetworkOption('net', 'net', '192.168.0.0') +>>> NetworkOption('net', 'net', '192.168.0.0/24', cidr=True) +>>> NetmaskOption('mask', 'mask', '255.255.255.0') + +>>> from tiramisu import BroadcastOption +>>> BroadcastOption('bcast', 'bcast', '192.168.0.254') + +>>> from tiramisu import PortOption +>>> PortOption('port', 'port', '80') +>>> PortOption('port', 'port', '2000', allow_range=True) +>>> PortOption('port', 'port', '2000:3000', allow_range=True) +>>> from tiramisu import PortOption +>>> try: +... PortOption('port', 'port', '0') +... except ValueError as err: +... print(err) +... +"0" is an invalid port for "port", must be between 1 and 49151 +>>> 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. + +>>> from tiramisu import PortOption +>>> PortOption('port', 'port', '80') +>>> try: +... PortOption('port', 'port', '80', allow_wellknown=False) +... except ValueError as err: +... print(err) +... +"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. + +>>> from tiramisu import PortOption +>>> PortOption('port', 'port', '1300') +>>> try: +... PortOption('port', 'port', '1300', allow_registred=False) +... except ValueError as err: +... print(err) +... +"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. + +>>> from tiramisu import PortOption +>>> try: +... PortOption('port', 'port', '64000') +... except ValueError as err: +... print(err) +... +"64000" is an invalid port for "port", must be between 1 and 49151 +>>> 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. + +Internet options +================================== + +.. list-table:: + :widths: 20 40 40 + :header-rows: 1 + + * - Type + - Comments + - Extra parameters + + * - DomainnameOption + - Domain names are used in various networking contexts and for application-specific naming and addressing purposes. + - + - 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 + + - allow_ip: the option can contain a domain name or an IP, in this case, IP is validate has IPOption would do. + - allow_cidr_network: the option can contain a CIDR network + - allow_without_dot: a domain name with domainname's type must have a dot, if active, we can set a domainname or an hostname + - 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, ...) + + * - URLOption + - An Uniform Resource Locator is, in fact, a string starting with http:// or https://, a DomainnameOption, optionaly ':' and a PortOption, and finally filename + - See PortOption and DomainnameOption parameters + + * - EmailOption + - Electronic mail (email or e-mail) is a method of exchanging messages ("mail") between people using electronic devices. + - + + +Examples +----------------------------------------------- + +>>> from tiramisu import DomainnameOption +>>> DomainnameOption('domain', 'domain', 'foo.example.net') +>>> DomainnameOption('domain', 'domain', 'foo', type='hostname') + +.. 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. + +>>> from tiramisu import DomainnameOption +>>> DomainnameOption('domain', 'domain', 'foo.example.net', allow_ip=True) +>>> DomainnameOption('domain', 'domain', '192.168.0.1', allow_ip=True) +>>> DomainnameOption('domain', 'domain', 'foo.example.net', allow_cidr_network=True) +>>> DomainnameOption('domain', 'domain', '192.168.0.0/24', allow_cidr_network=True) +>>> DomainnameOption('domain', 'domain', 'foo.example.net', allow_without_dot=True) +>>> DomainnameOption('domain', 'domain', 'foo', allow_without_dot=True) +>>> DomainnameOption('domain', 'domain', 'example.net', allow_startswith_dot=True) +>>> DomainnameOption('domain', 'domain', '.example.net', allow_startswith_dot=True) + +>>> 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') + +>>> from tiramisu import EmailOption +>>> EmailOption('mail', 'mail', 'foo@example.net') + +Unix options +=============== + +.. list-table:: + :widths: 20 40 + :header-rows: 1 + + * - Type + - Comments + + * - 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 "_". + + * - GroupnameOption + - Same conditions has username + + * - PasswordOption + - Simple string with no other restriction: + + * - FilenameOption + - For this option, only lowercase and uppercas ASCII character, "-", ".", "_", "~", and "/" are allowed. + +>>> from tiramisu import UsernameOption +>>> UsernameOption('user', 'user', 'my_user') + +>>> from tiramisu import GroupnameOption +>>> GroupnameOption('group', 'group', 'my_group') + +>>> from tiramisu import PasswordOption +>>> PasswordOption('pass', 'pass', 'oP$¨1jiJie') + +>>> from tiramisu import FilenameOption +>>> FilenameOption('file', 'file', '/etc/tiramisu/tiramisu.conf') + +Date option +============= + +Date option waits for a date with format YYYY-MM-DD: + +>>> from tiramisu import DateOption +>>> DateOption('date', 'date', '2019-10-30') + +Choice option: :class:`ChoiceOption` +====================================== + +Option that only accepts a list of possible choices. + +For example, we just want allowed 1 or 'see later': + +>>> 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: + +>>> try: +... ChoiceOption('choice', 'choice', (1, 'see later'), "i don't know") +... except ValueError as err: +... print(err) +... +"i don't know" is an invalid choice for "choice", only "1" and "see later" are allowed diff --git a/docs/own_option.rst b/docs/own_option.rst new file mode 100644 index 0000000..b7f6c9c --- /dev/null +++ b/docs/own_option.rst @@ -0,0 +1,125 @@ +====================================== +Create it's own option +====================================== + +Generic regexp option: :class:`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: + +.. literalinclude:: src/own_option.py + :lines: 3-11 + :linenos: + +Let's try our object: + +>>> VowelOption('vowel', 'Vowel', 'aae') + +>>> try: +... VowelOption('vowel', 'Vowel', 'oooups') +... except ValueError as err: +... print(err) +... +"oooups" is an invalid string with vowel for "Vowel" + +Create you own option +================================= + +An option always inherits from `Option` object. This object 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) + +Here an example to an lipogram option: + +.. literalinclude:: src/own_option2.py + :lines: 3-15 + :linenos: + +First of all we want to add a custom parameter to ask the minimum length (`min_len`) of the value: + +.. literalinclude:: src/own_option2.py + :lines: 16-20 + :linenos: + +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: + +.. literalinclude:: src/own_option2.py + :lines: 22-29 + :linenos: + +Even if user set warnings_only attribute, this method will raise. + +Finally we add a method to valid the value length. If `warnings_only` is set to True, a warning will be emit: + +.. literalinclude:: src/own_option2.py + :lines: 31-43 + :linenos: + +Let's test it: + +1. the character "e" is in the value: + +>>> try: +... LipogramOption('lipo', +... 'Lipogram', +... 'I just want to add a quality string that has no bad characters') +... 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? + +2. the character "e" is in the value and warnings_only is set to True: + +>>> 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 + +>>> try: +... LipogramOption('lipo', +... 'Lipogram', +... 'I just want to add a quality string that has no bad symbols') +... except ValueError as err: +... print(err) +... +"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: + +>>> 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) +... +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: + +>>> LipogramOption('lipo', +... 'Lipogram', +... 'I just want to add a quality string that has no bad symbols', +... min_len=50) + + diff --git a/docs/property.rst b/docs/property.rst new file mode 100644 index 0000000..ca1fa07 --- /dev/null +++ b/docs/property.rst @@ -0,0 +1,126 @@ +================================== +Properties +================================== + +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 (or for a :doc:`calculation`). +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 option in config are frozen (even if option have not frozen property). + +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. + + + +.. #FIXME +.. FORBIDDEN_SET_PERMISSIVES = frozenset(['force_default_on_freeze', +.. 'force_metaconfig_on_freeze', +.. 'force_store_value']) diff --git a/docs/quiz.rst b/docs/quiz.rst new file mode 100644 index 0000000..ee819e8 --- /dev/null +++ b/docs/quiz.rst @@ -0,0 +1,158 @@ +================================== +Bonus: Let's create a quiz! +================================== + +Creating our quiz +================================== + +Of course Tiramisu is great to handle options, but here's a little thing you can +do if you're just bored and don't know what to do. + +So, let's create a quiz. + +First, as always, let's import everything we need from Tiramisu and create our options: + +.. literalinclude:: src/quiz.py + :lines: 1-6 + :linenos: + +We make a dictionary with all questions, proposals and answer: + +.. literalinclude:: src/quiz.py + :lines: 20-35 + :linenos: + +Now build our Config. + +We have to create question option. + +Just after, we're going to define the correct answer in a second option. + +The answer is frozen so that when it is set, it cannot change. + +And finally a last option that will verify if the answer is correct. + + .. literalinclude:: src/quiz.py + :lines: 38-59 + :linenos: + +The `verif` option will us a function that will verify if the answer given by the user +(which will become the value of `question`) is the same as the correct answer (which is the value +of `answer`). Here is this function (of course you have to declare at the begining of your code, +before your options) : + +.. literalinclude:: src/quiz.py + :lines: 12-13 + :linenos: + +Pretty simple. + +At least we're done with our questions. Let's just create one last option. +This option calculate the result of the students' answers: + +.. literalinclude:: src/quiz.py + :lines: 16-17, 62-65 + :linenos: + +Now we just have to create our OptionDescription and Config (well it's a MetaConfig +here, but we'll see this later)... + +.. literalinclude:: src/quiz.py + :lines: 66, 9, 67 + :linenos: + +... and add some loops to run our quiz! + +.. literalinclude:: src/quiz.py + :lines: 70-104 + :linenos: + +Display results for teacher: + +.. literalinclude:: src/quiz.py + :lines: 107-117 + :linenos: + + +Now let's play ! + +.. literalinclude:: src/quiz.py + :lines: 120-132 + +Download the :download:`full code ` + +Hey, that was easy ! Almost like I already knew the answers... Oh wait... + +Get players results +================================== + +Now that you have your quiz, you can play with friends! And what's better than playing +with friends? Crushing them by comparing your scores of course! +You may have noticed that the previous code had some storage instructions. Now we can +create a score board that will give each player's latest score with their errors ! + +We created a meta config that will be used as a base for all configs we will create : +each time a new player name will be entered, a new config will be created, with the new player's +name as it's session id. This way, we can see every player's result ! + +So, earlier, we created a MetaConfig, and set the storage on sqlite3, so our data will +not be deleted after the quiz stops to run. + +Let's run the script: + + | Who are you? (a student | a teacher): a student + | Enter a name: my name + | Question 1: what does the cat say? + | woof | meow + | Your answer: meow + | Correct answer! + | + | Question 2: what do you get by mixing blue and yellow? + | green | red | purple + | Your answer: green + | Correct answer! + | + | Question 3: where is Bryan? + | at school | in his bedroom | in the kitchen + | Your answer: at school + | Wrong answer... the correct answer was: in the kitchen + | + | Question 4: which one has 4 legs and 2 wings? + | a wyvern | a dragon | a wyrm | a drake + | Your answer: a dragon + | Correct answer! + | + | Question 5: why life? + | because | I don't know | good question + | Your answer: good question + | Correct answer! + | + | Correct answers: 4 out of 5 + +When the quiz runs, we will create a new Config in our MetaConfig: + + .. literalinclude:: src/quiz.py + :lines: 70-75 + :linenos: + :emphasize-lines: 3 + +All results are store in this config (so in the database). So we need to reload those previous config: + + .. literalinclude:: src/quiz.py + :lines: 120-124 + :linenos: + :emphasize-lines: 4 + +Later, a teacher ca display all those score: + + | Who are you? (a student | a teacher): a teacher + | ==================== my name ========================== + | Question 1: correct answer + | Question 2: correct answer + | Question 3: wrong answer: at school + | Question 4: correct answer + | Question 5: correct answer + | my name's score: 4 out of 5 + +You've got everything now, so it's your turn to create your own questions and play with +your friends ! diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..87f1fb7 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,81 @@ +alabaster==0.7.13 +asttokens==2.4.1 +attrs==23.1.0 +Babel==2.13.1 +certifi==2023.7.22 +charset-normalizer==3.3.2 +click==8.1.7 +colorama==0.4.6 +comm==0.2.0 +debugpy==1.8.0 +decorator==5.1.1 +docutils==0.18.1 +exceptiongroup==1.1.3 +executing==2.0.1 +fastjsonschema==2.18.1 +greenlet==3.0.1 +idna==3.4 +imagesize==1.4.1 +importlib-metadata==6.8.0 +ipykernel==6.26.0 +ipython==8.17.2 +jedi==0.19.1 +Jinja2==3.1.2 +jsonschema==4.19.2 +jsonschema-specifications==2023.7.1 +jupyter-cache==1.0.0 +jupyter_client==8.6.0 +jupyter_core==5.5.0 +livereload==2.6.3 +markdown-it-py==3.0.0 +MarkupSafe==2.1.3 +matplotlib-inline==0.1.6 +mdit-py-plugins==0.4.0 +mdurl==0.1.2 +myst-nb==1.0.0 +myst-parser==2.0.0 +nbclient==0.9.0 +nbformat==5.9.2 +nest-asyncio==1.5.8 +packaging==23.2 +parso==0.8.3 +pexpect==4.8.0 +platformdirs==4.0.0 +prompt-toolkit==3.0.40 +psutil==5.9.6 +ptyprocess==0.7.0 +pure-eval==0.2.2 +Pygments==2.16.1 +python-dateutil==2.8.2 +PyYAML==6.0.1 +pyzmq==25.1.1 +referencing==0.30.2 +requests==2.31.0 +rpds-py==0.12.0 +six==1.16.0 +snowballstemmer==2.2.0 +Sphinx==7.2.6 +sphinx-autobuild==2021.3.14 +sphinx-copybutton==0.5.2 +sphinx-lesson==0.8.15 +sphinx-minipres==0.2.1 +sphinx-rtd-theme==1.3.0 +sphinx-rtd-theme-ext-color-contrast==0.3.1 +sphinx-tabs==3.4.4 +sphinx-togglebutton==0.3.2 +sphinxcontrib-applehelp==1.0.7 +sphinxcontrib-devhelp==1.0.5 +sphinxcontrib-htmlhelp==2.0.4 +sphinxcontrib-jquery==4.1 +sphinxcontrib-jsmath==1.0.1 +sphinxcontrib-qthelp==1.0.6 +sphinxcontrib-serializinghtml==1.1.9 +SQLAlchemy==2.0.23 +stack-data==0.6.3 +tabulate==0.9.0 +tornado==6.3.3 +traitlets==5.13.0 +typing_extensions==4.8.0 +urllib3==2.0.7 +wcwidth==0.2.9 +zipp==3.17.0 diff --git a/docs/src/Makefile b/docs/src/Makefile new file mode 100644 index 0000000..6e67387 --- /dev/null +++ b/docs/src/Makefile @@ -0,0 +1,18 @@ +SRC=$(wildcard *.py) +HTMLFRAGMENT=$(addsuffix .html, $(basename $(SRC))) + +.SUFFIXES: + +.PHONY: all clean + +all: html +# make -C ./build all + +html: $(HTMLFRAGMENT) + +%.html: %.py + pygmentize -f html -O full,bg=white,style=bw, -o ../en/_build/html/_modules/$@ $< + +clean: +# make -C ./build clean + diff --git a/docs/src/api_global_permissive.py b/docs/src/api_global_permissive.py new file mode 100644 index 0000000..f2a6599 --- /dev/null +++ b/docs/src/api_global_permissive.py @@ -0,0 +1,28 @@ +from os.path import exists +from tiramisu import FilenameOption, BoolOption, OptionDescription, Leadership, \ + Config, Calculation, Params, ParamOption, ParamValue, calc_value +from tiramisu.error import PropertiesOptionError, ConfigError + + +def inverse(exists_): + return not exists_ + + +filename = FilenameOption('filename', + 'Filename', + multi=True, + properties=('mandatory',)) +exists_ = BoolOption('exists', + 'This file exists', + Calculation(exists, Params(ParamOption(filename))), + multi=True, + properties=('frozen', 'force_default_on_freeze', 'advanced')) +create = BoolOption('create', + 'Create automaticly the file', + multi=True, + default_multi=Calculation(inverse, Params(ParamOption(exists_)))) +new = Leadership('new', + 'Add new file', + [filename, exists_, create]) +root = OptionDescription('root', 'root', [new]) +config = Config(root) diff --git a/docs/src/api_global_property.py b/docs/src/api_global_property.py new file mode 100644 index 0000000..01e26a3 --- /dev/null +++ b/docs/src/api_global_property.py @@ -0,0 +1,20 @@ +from os.path import exists +from tiramisu import FilenameOption, BoolOption, OptionDescription, Leadership, \ + Config, Calculation, Params, ParamOption +from tiramisu.error import PropertiesOptionError + + +filename = FilenameOption('filename', + 'Filename', + multi=True, + properties=('mandatory',)) +exists_ = BoolOption('exists', + 'This file exists', + Calculation(exists, Params(ParamOption(filename))), + multi=True, + properties=('frozen', 'force_default_on_freeze', 'advanced')) +new = Leadership('new', + 'Add new file', + [filename, exists_]) +root = OptionDescription('root', 'root', [new]) +config = Config(root) diff --git a/docs/src/api_option_property.py b/docs/src/api_option_property.py new file mode 100644 index 0000000..bbf3ef6 --- /dev/null +++ b/docs/src/api_option_property.py @@ -0,0 +1,147 @@ +from stat import S_IMODE, S_ISDIR, S_ISSOCK +from os import lstat, getuid, getgid +from os.path import exists +from pwd import getpwuid +from grp import getgrgid +from tiramisu import FilenameOption, UsernameOption, GroupnameOption, IntOption, BoolOption, ChoiceOption, \ + OptionDescription, Leadership, Config, Calculation, Params, ParamSelfOption, ParamOption, ParamValue, \ + calc_value +from tiramisu.error import LeadershipError, PropertiesOptionError + + +def get_username(filename, exists, create=False): + if exists: + uid = lstat(filename).st_uid + elif create: + # the current uid + uid = getuid() + else: + return + + return getpwuid(uid).pw_name + + +def get_grpname(filename, exists, create=False): + if exists: + gid = lstat(filename).st_gid + elif create: + # the current gid + gid = getgid() + else: + return + return getgrgid(gid).gr_name + + +def calc_type(filename, is_exists): + if is_exists: + mode = lstat(filename).st_mode + if S_ISSOCK(mode): + return 'socket' + elif S_ISDIR(mode): + return 'directory' + return 'file' + + +def calc_mode(filename, is_exists, type): + if is_exists: + return int(oct(S_IMODE(lstat(filename).st_mode))[2:]) + if type == 'file': + return 644 + elif type == 'directory': + return 755 + elif type == 'socket': + return 444 + + +filename = FilenameOption('filename', + 'Filename', + multi=True, + properties=('mandatory',)) +exists_ = BoolOption('exists', + 'This file exists', + Calculation(exists, Params(ParamOption(filename))), + multi=True, + properties=('mandatory', 'frozen', 'force_default_on_freeze', 'advanced')) +create = BoolOption('create', + 'Create automaticly the file', + multi=True, + default_multi=True, + properties=(Calculation(calc_value, + Params(ParamValue('disabled'), + kwargs={'condition': ParamOption(exists_), + 'expected': ParamValue(True)})),)) +type_ = ChoiceOption('type', + 'The file type', + ('file', 'directory', 'socket'), + Calculation(calc_type, Params((ParamOption(filename), + ParamOption(exists_)))), + multi=True, + properties=('force_default_on_freeze', 'mandatory', + Calculation(calc_value, + Params(ParamValue('hidden'), + kwargs={'condition': ParamOption(exists_), + 'expected': ParamValue(True)})), + Calculation(calc_value, + Params(ParamValue('frozen'), + kwargs={'condition': ParamOption(exists_), + 'expected': ParamValue(True)})))) +username = UsernameOption('user', + 'User', + default_multi=Calculation(get_username, Params((ParamOption(filename), + ParamOption(exists_), + ParamOption(create, notraisepropertyerror=True)))), + multi=True, + properties=('force_store_value', + Calculation(calc_value, + Params(ParamValue('mandatory'), + kwargs={'condition': ParamOption(create, notraisepropertyerror=True), + 'expected': ParamValue(True), + 'no_condition_is_invalid': ParamValue(True)})),)) +grpname = GroupnameOption('group', + 'Group', + default_multi=Calculation(get_grpname, Params((ParamOption(filename), + ParamOption(exists_), + ParamOption(create, notraisepropertyerror=True)))), + multi=True, + properties=('force_store_value', + Calculation(calc_value, + Params(ParamValue('mandatory'), + kwargs={'condition': ParamOption(create, notraisepropertyerror=True), + 'expected': ParamValue(True), + 'no_condition_is_invalid': ParamValue(True)})),)) +mode = IntOption('mode', + 'Mode', + default_multi=Calculation(calc_mode, Params((ParamOption(filename), ParamOption(exists_), ParamOption(type_)))), + multi=True, + properties=('mandatory', 'advanced', 'force_store_value')) + +new = Leadership('new', + 'Add new file', + [filename, exists_, create, type_, username, grpname, mode]) + +root = OptionDescription('root', 'root', [new]) + +config = Config(root) + + + + +#config.option('new.create', 1).value.set(False) +#config.option('new.type', 1).value.set('file') +#config.option('new.type', 2).value.set('file') +#print(config.value.dict()) +#config.option('new.type', 2).value.set('directory') +#print(config.value.dict()) +#print(config.unrestraint.option('new.mode', 0).owner.isdefault()) +#print(config.unrestraint.option('new.mode', 1).owner.isdefault()) +#print(config.unrestraint.option('new.mode', 2).owner.isdefault()) +#config.property.read_only() +#print(config.option('new.mode', 0).owner.isdefault()) +#print(config.option('new.mode', 1).owner.isdefault()) +#print(config.option('new.mode', 2).owner.isdefault()) +#print(config.value.dict()) +#config.property.read_write() +#config.option('new.type', 2).value.set('file') +#print(config.value.dict()) +#config.option('new.mode', 2).value.reset() +#print(config.value.dict()) diff --git a/docs/src/api_value.py b/docs/src/api_value.py new file mode 100644 index 0000000..1ad75bb --- /dev/null +++ b/docs/src/api_value.py @@ -0,0 +1,31 @@ +from shutil import disk_usage +from os.path import isdir +from tiramisu import FilenameOption, FloatOption, OptionDescription, Config, \ + Calculation, Params, ParamValue, ParamOption, ParamSelfOption + +def valid_is_dir(path): + # verify if path is a directory + if not isdir(path): + raise ValueError('this directory does not exist') + +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 + + +filename = FilenameOption('path', 'Path', validators=[Calculation(valid_is_dir, + Params(ParamSelfOption()))]) +usage = FloatOption('usage', 'Disk usage', Calculation(calc_disk_usage, + Params(ParamOption(filename)))) +disk = OptionDescription('disk', 'Verify disk usage', [filename, usage]) +root = OptionDescription('root', 'root', [disk]) +config = Config(root) +config.property.read_write() diff --git a/docs/src/api_value_choice.py b/docs/src/api_value_choice.py new file mode 100644 index 0000000..2be56bd --- /dev/null +++ b/docs/src/api_value_choice.py @@ -0,0 +1,33 @@ +from shutil import disk_usage +from os.path import isdir +from tiramisu import FilenameOption, FloatOption, ChoiceOption, OptionDescription, Config, \ + Calculation, Params, ParamValue, ParamOption, ParamSelfOption + +def valid_is_dir(path): + # verify if path is a directory + if not isdir(path): + raise ValueError('this directory does not exist') + +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 + + +filename = FilenameOption('path', 'Path', validators=[Calculation(valid_is_dir, + Params(ParamSelfOption()))]) +size_type = ChoiceOption('size_type', 'Size type', ('bytes', 'giga bytes'), 'bytes') +usage = FloatOption('usage', 'Disk usage', Calculation(calc_disk_usage, + Params((ParamOption(filename), + ParamOption(size_type))))) +disk = OptionDescription('disk', 'Verify disk usage', [filename, size_type, usage]) +root = OptionDescription('root', 'root', [disk]) +config = Config(root) +config.property.read_write() diff --git a/docs/src/api_value_leader.py b/docs/src/api_value_leader.py new file mode 100644 index 0000000..115167f --- /dev/null +++ b/docs/src/api_value_leader.py @@ -0,0 +1,34 @@ +from shutil import disk_usage +from os.path import isdir +from tiramisu import FilenameOption, FloatOption, ChoiceOption, OptionDescription, Leadership, \ + Config, \ + Calculation, Params, ParamValue, ParamOption, ParamSelfOption + +def valid_is_dir(path): + # verify if path is a directory + if not isdir(path): + raise ValueError('this directory does not exist') + +def calc_disk_usage(path, size): + if size == 'bytes': + div = 1 + else: + # bytes to gigabytes + div = 1024 * 1024 * 1024 + return disk_usage(path).free / div + + +filename = FilenameOption('path', 'Path', validators=[Calculation(valid_is_dir, + Params(ParamSelfOption(whole=False)))], + multi=True) +size_type = ChoiceOption('size_type', 'Size type', ('bytes', 'giga bytes'), + default_multi='bytes', multi=True) +usage = FloatOption('usage', 'Disk usage', Calculation(calc_disk_usage, + Params((ParamOption(filename), + ParamOption(size_type)))), + multi=True) +disk = Leadership('disk', 'Verify disk usage', [filename, size_type, usage]) +root = OptionDescription('root', 'root', [disk]) +config = Config(root) +config.property.read_write() + diff --git a/docs/src/api_value_multi.py b/docs/src/api_value_multi.py new file mode 100644 index 0000000..07ba5c8 --- /dev/null +++ b/docs/src/api_value_multi.py @@ -0,0 +1,34 @@ +from shutil import disk_usage +from os.path import isdir +from tiramisu import FilenameOption, FloatOption, ChoiceOption, OptionDescription, Config, \ + Calculation, Params, ParamValue, ParamOption, ParamSelfOption + +def valid_is_dir(path): + # verify if path is a directory + if not isdir(path): + raise ValueError('this directory does not exist') + +def calc_disk_usage(paths, size='bytes'): + if size == 'bytes': + div = 1 + else: + # bytes to gigabytes + div = 1024 * 1024 * 1024 + ret = [] + for path in paths: + ret.append(disk_usage(path).free / div) + return ret + + +filename = FilenameOption('path', 'Path', validators=[Calculation(valid_is_dir, + Params(ParamSelfOption(whole=False)))], + multi=True) +size_type = ChoiceOption('size_type', 'Size type', ('bytes', 'giga bytes'), 'bytes') +usage = FloatOption('usage', 'Disk usage', Calculation(calc_disk_usage, + Params((ParamOption(filename), + ParamOption(size_type)))), + multi=True) +disk = OptionDescription('disk', 'Verify disk usage', [filename, size_type, usage]) +root = OptionDescription('root', 'root', [disk]) +config = Config(root) +config.property.read_write() diff --git a/docs/src/application.py b/docs/src/application.py new file mode 100644 index 0000000..94e84c8 --- /dev/null +++ b/docs/src/application.py @@ -0,0 +1,232 @@ +from tiramisu import BoolOption, ChoiceOption, DomainnameOption, PortOption, URLOption, \ + OptionDescription, Calculation, Params, ParamOption, ParamValue, \ + Config, calc_value, calc_value_property_help + + +def protocols_settings(use: bool, value): + if use is True: + return value + + +# this option's value will determine which of the others options are frozen and which are not thanks +proxy_mode = ChoiceOption('proxy_mode', + 'Proxy\'s config mode', + ('No proxy', + 'Auto-detect proxy settings for this network', + 'Use system proxy settings', + 'Manual proxy configuration', + 'Automatic proxy configuration URL'), + default = 'No proxy', + properties=('mandatory',)) + + +http_address = DomainnameOption('http_address', + 'Address', + allow_ip=True, + properties=('mandatory',)) +http_port = PortOption('http_port', + 'Port', + default='8080', + properties=('mandatory',)) +http_proxy = OptionDescription('http_proxy', + 'HTTP Proxy', + [http_address, http_port]) + +use_for_all_protocols = BoolOption('use_for_all_protocols', + 'Use HTTP IP and Port for all protocols', + default=True) + + +# if this option is valued with 'True', set all the others IP and port values to the same as HTTP IP and port. +ssl_address = DomainnameOption('ssl_address', + 'Address', + Calculation(protocols_settings, + Params((ParamOption(use_for_all_protocols), ParamOption(http_address)))), + allow_ip=True, + properties=('mandatory', 'force_default_on_freeze', + Calculation(calc_value, + Params(ParamValue('frozen'), + kwargs={'condition': ParamOption(use_for_all_protocols, todict=True), + 'expected': ParamValue(True)}), + calc_value_property_help))) +ssl_port = PortOption('ssl_port', + 'Port', + Calculation(protocols_settings, + Params((ParamOption(use_for_all_protocols), ParamOption(http_port)))), + properties=('mandatory', 'force_default_on_freeze', + Calculation(calc_value, + Params(ParamValue('frozen'), + kwargs={'condition': ParamOption(use_for_all_protocols, todict=True), + 'expected': ParamValue(True)}), + calc_value_property_help))) +ssl_proxy = OptionDescription('ssl_proxy', + 'SSL Proxy', + [ssl_address, ssl_port], + properties=(Calculation(calc_value, + Params(ParamValue('hidden'), + kwargs={'condition': ParamOption(use_for_all_protocols, todict=True), + 'expected': ParamValue(True)}), + calc_value_property_help),)) + +ftp_address = DomainnameOption('ftp_address', + 'Address', + Calculation(protocols_settings, + Params((ParamOption(use_for_all_protocols), ParamOption(http_address)))), + allow_ip=True, + properties=('mandatory', 'force_default_on_freeze', + Calculation(calc_value, + Params(ParamValue('frozen'), + kwargs={'condition': ParamOption(use_for_all_protocols, todict=True), + 'expected': ParamValue(True)}), + calc_value_property_help))) +ftp_port = PortOption('ftp_port', + 'Port', + Calculation(protocols_settings, + Params((ParamOption(use_for_all_protocols), ParamOption(http_port)))), + properties=('force_default_on_freeze', + Calculation(calc_value, + Params(ParamValue('frozen'), + kwargs={'condition': ParamOption(use_for_all_protocols, todict=True), + 'expected': ParamValue(True)}), + calc_value_property_help))) +ftp_proxy = OptionDescription('ftp_proxy', + 'FTP Proxy', + [ftp_address, ftp_port], + properties=(Calculation(calc_value, + Params(ParamValue('hidden'), + kwargs={'condition': ParamOption(use_for_all_protocols, todict=True), + 'expected': ParamValue(True)}), + calc_value_property_help),)) + +socks_address = DomainnameOption('socks_address', + 'Address', + Calculation(protocols_settings, + Params((ParamOption(use_for_all_protocols), ParamOption(http_address)))), + allow_ip=True, + properties=('mandatory', 'force_default_on_freeze', + Calculation(calc_value, + Params(ParamValue('frozen'), + kwargs={'condition': ParamOption(use_for_all_protocols, todict=True), + 'expected': ParamValue(True)}), + calc_value_property_help))) +socks_port = PortOption('socks_port', + 'Port', + Calculation(protocols_settings, + Params((ParamOption(use_for_all_protocols), ParamOption(http_port)))), + properties=('mandatory', 'force_default_on_freeze', + Calculation(calc_value, + Params(ParamValue('frozen'), + kwargs={'condition': ParamOption(use_for_all_protocols, todict=True), + 'expected': ParamValue(True)}), + calc_value_property_help))) +socks_version = ChoiceOption('socks_version', + 'SOCKS host version used by proxy', + ('v4', 'v5'), + default='v5', + properties=('force_default_on_freeze', + Calculation(calc_value, + Params(ParamValue('frozen'), + kwargs={'condition': ParamOption(use_for_all_protocols, todict=True), + 'expected': ParamValue(True)}), + calc_value_property_help))) +socks_proxy = OptionDescription('socks_proxy', + 'Socks host proxy', + [socks_address, socks_port, socks_version], + properties=(Calculation(calc_value, + Params(ParamValue('hidden'), + kwargs={'condition': ParamOption(use_for_all_protocols, todict=True), + 'expected': ParamValue(True)}), + calc_value_property_help),)) +protocols = OptionDescription('protocols', + 'Protocols parameters', + [http_proxy, + use_for_all_protocols, + ssl_proxy, + ftp_proxy, + socks_proxy], + properties=(Calculation(calc_value, + Params(ParamValue('disabled'), + kwargs={'condition': ParamOption(proxy_mode, todict=True), + 'expected': ParamValue('Manual proxy configuration'), + 'reverse_condition': ParamValue(True)}), + calc_value_property_help),)) + +auto_config_url = URLOption('auto_config_url', + 'Proxy\'s auto config URL', + allow_ip=True, + properties=('mandatory', + Calculation(calc_value, + Params(ParamValue('disabled'), + kwargs={'condition': ParamOption(proxy_mode, todict=True), + 'expected': ParamValue('Automatic proxy configuration URL'), + 'reverse_condition': ParamValue(True)}), + calc_value_property_help),)) + +no_proxy = DomainnameOption('no_proxy', + 'Address for which proxy will be desactivated', + multi=True, + allow_ip=True, + allow_cidr_network=True, + allow_without_dot=True, + allow_startswith_dot=True, + properties=(Calculation(calc_value, + Params(ParamValue('disabled'), + kwargs={'condition': ParamOption(proxy_mode, todict=True), + 'expected': ParamValue('No proxy')}), + calc_value_property_help),)) + +prompt_authentication = BoolOption('prompt_authentication', + 'Prompt for authentication if password is saved', + default=False, + properties=(Calculation(calc_value, + Params(ParamValue('disabled'), + kwargs={'condition': ParamOption(proxy_mode, todict=True), + 'expected': ParamValue('No proxy')}), + calc_value_property_help),)) +proxy_dns_socks5 = BoolOption('proxy_dns_socks5', + 'Use Proxy DNS when using SOCKS v5', + default=False, + properties=(Calculation(calc_value, + Params(ParamValue('disabled'), + kwargs={'condition_1': ParamOption(socks_version, + raisepropertyerror=True), + 'expected_1': ParamValue('v4'), + 'condition_2': ParamOption(proxy_mode, todict=True), + 'expected_2': ParamValue('No proxy'), + 'condition_operator': ParamValue('OR')}), + calc_value_property_help),)) +enable_dns_over_https = BoolOption('enable_dns_over_https', + 'Enable DNS over HTTPS', + default=False) + +used_dns = ChoiceOption('used_dns', + 'Used DNS', + ('default', 'custom'), + properties=(Calculation(calc_value, + Params(ParamValue('disabled'), + kwargs={'condition': ParamOption(enable_dns_over_https, todict=True), + 'expected': ParamValue(False)}), + calc_value_property_help),)) + +custom_dns_url = URLOption('custom_dns_url', + 'Custom DNS URL', + properties=(Calculation(calc_value, + Params(ParamValue('disabled'), + kwargs={'condition': ParamOption(used_dns, todict=True, + raisepropertyerror=True), + 'expected': ParamValue('default')}), + calc_value_property_help),)) +dns_over_https = OptionDescription('dns_over_https', + 'DNS over HTTPS', + [enable_dns_over_https, used_dns, custom_dns_url]) + +rootod = OptionDescription('proxy', + 'Proxy parameters', + [proxy_mode, + protocols, + no_proxy, + auto_config_url, + prompt_authentication, + proxy_dns_socks5, dns_over_https]) +proxy_config = Config(rootod) +proxy_config.property.read_write() diff --git a/docs/src/calculation.py b/docs/src/calculation.py new file mode 100644 index 0000000..0b626df --- /dev/null +++ b/docs/src/calculation.py @@ -0,0 +1,73 @@ +from tiramisu import Config, OptionDescription, Leadership, IntOption, Params, ParamOption, ParamValue, ParamContext, ParamIndex + +def a_function(): + pass +Calculation(a_function) + + +def a_function_with_parameters(value1, value2): + return value1 + ' ' + value2 +Calculation(a_function_with_parameters, Params(ParamValue('my value 1'), kwargs={value2: ParamValue('my value 2')})) + + +def a_function_with_parameters(value1, value2): + return value1 + ' ' + value2 +Calculation(a_function_with_parameters, Params((ParamValue('my value 1'), ParamValue('my value 2')))) + + +def a_function_with_option(option1): + return option1 +option1 = IntOption('option1', 'first option', 1) +Calculation(a_function_with_option, Params(ParamOption(option1))) + + +def a_function_with_option(option1): + return option1 +option1 = IntOption('option1', 'first option', 1, properties=('disabled',)) +Calculation(a_function_with_option, Params(ParamOption(option1))) + +def a_function_with_option(option1): + return option1 +Calculation(a_function_with_option, Params(ParamOption(option1, raisepropertyerror=True))) + +def a_function_with_option(option1=None): + return option1 +Calculation(a_function_with_option, Params(ParamOption(option1, notraisepropertyerror=True))) + +def a_function_with_dict_option(option1): + return "the option {} has value {}".format(option1['name'], option1['value']) +Calculation(a_function_with_option, Params(ParamOption(todict=True))) + + +def a_function_with_context(context): + pass +Calculation(a_function_with_context, Params(ParamContext())) + +def a_function_multi(option1): + return option1 +option1 = IntOption('option1', 'option1', [1], multi=True) +Calculation(a_function, Params(ParamOption(option1))) + +def a_function_leader(option): + return option +leader = IntOption('leader', 'leader', [1], multi=True) +follower1 = IntOption('follower1', 'follower1', default_multi=2, multi=True) +follower2 = IntOption('follower2', 'follower2', default_multi=3, multi=True) +leadership = Leadership('leadership', 'leadership', [leader, follower1, follower2]) +Calculation(a_function_leader, Params(ParamOption(leader))) + +def a_function_follower(follower): + return follower +leader = IntOption('leader', 'leader', [1], multi=True) +follower1 = IntOption('follower1', 'follower1', default_multi=2, multi=True) +follower2 = IntOption('follower2', 'follower2', default_multi=3, multi=True) +leadership = Leadership('leadership', 'leadership', [leader, follower1, follower2]) +Calculation(a_function_follower, Params(ParamOption(follower1))) + +def a_function_index(index): + return index +leader = IntOption('leader', 'leader', [1], multi=True) +follower1 = IntOption('follower1', 'follower1', default_multi=2, multi=True) +follower2 = IntOption('follower2', 'follower2', default_multi=3, multi=True) +leadership = Leadership('leadership', 'leadership', [leader, follower1, follower2]) +Calculation(a_function_index, Params(ParamIndex())) diff --git a/docs/src/find.py b/docs/src/find.py new file mode 100644 index 0000000..d5f9b2a --- /dev/null +++ b/docs/src/find.py @@ -0,0 +1,6 @@ +"this is only to make sure that the tiramisu library loads properly" + +cfg = Config(rootod) + + + diff --git a/docs/src/getting_started.py b/docs/src/getting_started.py new file mode 100644 index 0000000..5b89b95 --- /dev/null +++ b/docs/src/getting_started.py @@ -0,0 +1,19 @@ +"getting started with the tiramisu library (it loads and prints properly)" + +from tiramisu import Config +from tiramisu import OptionDescription, BoolOption + +# let's create a group of options +descr = OptionDescription("optgroup", "", [ + # ... with only one option inside + BoolOption("bool", "", default=False) + ]) + +cfg = Config(descr) + +# the global help about the config +cfg.help() +# help about an option +cfg.option.help() +# the config's __repr__ +print(cfg) diff --git a/docs/src/own_option.py b/docs/src/own_option.py new file mode 100644 index 0000000..f31d891 --- /dev/null +++ b/docs/src/own_option.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 + +import re +from tiramisu import RegexpOption + + +class VowelOption(RegexpOption): + __slots__ = tuple() + _type = 'vowel' + _display_name = "string with vowel" + _regexp = re.compile(r"^[aeiouy]*$") diff --git a/docs/src/own_option2.py b/docs/src/own_option2.py new file mode 100644 index 0000000..304d86e --- /dev/null +++ b/docs/src/own_option2.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 + +from tiramisu import Option +from tiramisu.error import ValueWarning +import warnings + + +class LipogramOption(Option): + __slots__ = tuple() + _type = 'lipogram' + _display_name = 'lipogram' + def __init__(self, + *args, + min_len=100, + **kwargs): + # store extra parameters + extra = {'_min_len': min_len} + super().__init__(*args, + extra=extra, + **kwargs) + + 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?') + + 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) diff --git a/docs/src/property.py b/docs/src/property.py new file mode 100644 index 0000000..2fa2514 --- /dev/null +++ b/docs/src/property.py @@ -0,0 +1,20 @@ +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', u'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]) + +# let's create the config +cfg = Config(rootod) +# the api is read only +cfg.property.read_only() +# the read_write api is available +cfg.property.read_write() + diff --git a/docs/src/proxy.py b/docs/src/proxy.py new file mode 100644 index 0000000..9116b80 --- /dev/null +++ b/docs/src/proxy.py @@ -0,0 +1,51 @@ +from tiramisu import IPOption, PortOption, BoolOption, ChoiceOption, DomainnameOption, \ + URLOption, NetworkOption, NetmaskOption, \ + SymLinkOption, OptionDescription, Leadership, Config + + +proxy_mode = ChoiceOption('proxy_mode', 'Proxy\'s config mode', ('No proxy', + 'Manual proxy configuration', + 'Automatic proxy configuration URL'), + properties=('positional', 'mandatory')) +http_ip_address = IPOption('http_ip_address', 'Proxy\'s HTTP IP', properties=('mandatory',)) +http_ip_short = SymLinkOption('i', http_ip_address) +http_port = PortOption('http_port', 'Proxy\'s HTTP Port', default='8080', properties=('mandatory',)) +http_port_short = SymLinkOption('p', http_port) +manual_proxy = OptionDescription('manual_proxy', 'Manual proxy settings', [http_ip_address, http_ip_short, http_port, http_port_short], + requires=[{'option': proxy_mode, 'expected': 'Manual proxy configuration', 'action':'disabled', 'inverse':True}]) + +auto_config_url = URLOption('auto_config_url','Proxy\'s auto config URL', properties=('mandatory',)) +auto_config_url_short = SymLinkOption('i', auto_config_url) +automatic_proxy = OptionDescription('automatic_proxy', 'Automatic proxy setting', + [auto_config_url, auto_config_url_short], + requires=[{'option': proxy_mode, 'expected': 'Automatic proxy configuration URL', 'action':'disabled', 'inverse': True}]) + +configuration = OptionDescription('configuration', None, + [manual_proxy, automatic_proxy]) + +no_proxy_domain = DomainnameOption('no_proxy_domain', 'Domain names for which proxy will be desactivated', multi=True) +no_proxy_network = NetworkOption('no_proxy_network', 'Network addresses', multi=True) +no_proxy_network_short = SymLinkOption('n', no_proxy_network) +no_proxy_netmask = NetmaskOption('no_proxy_netmask', 'Netmask addresses', multi=True, properties=('mandatory',)) +no_proxy_network_leadership = Leadership('no_proxy_network', 'Network for which proxy will be desactivated', [no_proxy_network, no_proxy_netmask]) +no_proxy = OptionDescription('no_proxy', 'Disabled proxy', + [no_proxy_domain, no_proxy_network_leadership], + requires=[{'option': proxy_mode, 'expected': 'No proxy', 'action':'disabled'}, {'option': proxy_mode, 'expected': None, 'action':'disabled'}]) + +dns_over_https = BoolOption('dns_over_https', 'Enable DNS over HTTPS', default=False) + +root = OptionDescription('proxy', 'Proxy parameters', + [proxy_mode, configuration, no_proxy, dns_over_https]) + +def display_name(option, dyn_name): + return "--" + option.impl_getpath() + +proxy_config = Config(root, display_name=display_name) +proxy_config.property.read_write() + +from tiramisu_cmdline_parser import TiramisuCmdlineParser +parser = TiramisuCmdlineParser(proxy_config) +parser.parse_args() + +from pprint import pprint +pprint(proxy_config.value.dict()) diff --git a/docs/src/proxy_persistent.py b/docs/src/proxy_persistent.py new file mode 100644 index 0000000..8d40a07 --- /dev/null +++ b/docs/src/proxy_persistent.py @@ -0,0 +1,54 @@ +from tiramisu import IPOption, PortOption, BoolOption, ChoiceOption, DomainnameOption, \ + URLOption, NetworkOption, NetmaskOption, \ + SymLinkOption, OptionDescription, Leadership, Config + + +proxy_mode = ChoiceOption('proxy_mode', 'Proxy\'s config mode', ('No proxy', + 'Manual proxy configuration', + 'Automatic proxy configuration URL'), + properties=('positional', 'mandatory')) +http_ip_address = IPOption('http_ip_address', 'Proxy\'s HTTP IP', properties=('mandatory',)) +http_ip_short = SymLinkOption('i', http_ip_address) +http_port = PortOption('http_port', 'Proxy\'s HTTP Port', default='8080', properties=('mandatory',)) +http_port_short = SymLinkOption('p', http_port) +manual_proxy = OptionDescription('manual_proxy', 'Manual proxy settings', [http_ip_address, http_ip_short, http_port, http_port_short], + requires=[{'option': proxy_mode, 'expected': 'Manual proxy configuration', 'action':'disabled', 'inverse':True}]) + +auto_config_url = URLOption('auto_config_url','Proxy\'s auto config URL', properties=('mandatory',)) +auto_config_url_short = SymLinkOption('i', auto_config_url) +automatic_proxy = OptionDescription('automatic_proxy', 'Automatic proxy setting', + [auto_config_url, auto_config_url_short], + requires=[{'option': proxy_mode, 'expected': 'Automatic proxy configuration URL', 'action':'disabled', 'inverse': True}]) + +configuration = OptionDescription('configuration', None, + [manual_proxy, automatic_proxy]) + +no_proxy_domain = DomainnameOption('no_proxy_domain', 'Domain names for which proxy will be desactivated', multi=True) +no_proxy_network = NetworkOption('no_proxy_network', 'Network addresses', multi=True) +no_proxy_network_short = SymLinkOption('n', no_proxy_network) +no_proxy_netmask = NetmaskOption('no_proxy_netmask', 'Netmask addresses', multi=True, properties=('mandatory',)) +no_proxy_network_leadership = Leadership('no_proxy_network', 'Network for which proxy will be desactivated', [no_proxy_network, no_proxy_netmask]) +no_proxy = OptionDescription('no_proxy', 'Disabled proxy', + [no_proxy_domain, no_proxy_network_leadership], + requires=[{'option': proxy_mode, 'expected': 'No proxy', 'action':'disabled'}, {'option': proxy_mode, 'expected': None, 'action':'disabled'}]) + +dns_over_https = BoolOption('dns_over_https', 'Enable DNS over HTTPS', default=False) + +root = OptionDescription('proxy', 'Proxy parameters', + [proxy_mode, configuration, no_proxy, dns_over_https]) + +def display_name(option, dyn_name): + return "--" + option.impl_getpath() + +from tiramisu import default_storage +default_storage.setting(engine='sqlite3') + +proxy_config = Config(root, display_name=display_name, persistent=True, session_id='proxy') +proxy_config.property.read_write() + +from tiramisu_cmdline_parser import TiramisuCmdlineParser +parser = TiramisuCmdlineParser(proxy_config) +parser.parse_args(valid_mandatory=False) + +from pprint import pprint +pprint(proxy_config.value.dict()) diff --git a/docs/src/quiz.py b/docs/src/quiz.py new file mode 100644 index 0000000..edb276e --- /dev/null +++ b/docs/src/quiz.py @@ -0,0 +1,132 @@ +from sys import exit +from tiramisu import (MetaConfig, Config, OptionDescription, + BoolOption, ChoiceOption, StrOption, IntOption, + Calculation, Params, ParamOption, + default_storage, list_sessions) +from tiramisu.error import ConflictError + + +default_storage.setting(engine="sqlite3") + + +def verif(q: str, a: str): + return q == a + + +def results(*verif): + return sum(verif) + + +questions = [{'description': 'what does the cat say?', + 'proposal': ('woof', 'meow'), + 'answer': 'meow'}, + {'description': 'what do you get by mixing blue and yellow?', + 'proposal': ('green', 'red', 'purple'), + 'answer': 'green'}, + {'description': 'where is Bryan?', + 'proposal': ('at school', 'in his bedroom', 'in the kitchen'), + 'answer': 'in the kitchen'}, + {'description': 'which one has 4 legs and 2 wings?', + 'proposal': ('a wyvern', 'a dragon', 'a wyrm', 'a drake'), + 'answer': 'a dragon'}, + {'description': 'why life?', + 'proposal': ('because', 'I don\'t know', 'good question'), + 'answer': 'good question'}, + ] + + +options_obj = [] +results_obj = [] +for idx, question in enumerate(questions): + idx += 1 + choice = ChoiceOption('question', + question['description'], + question['proposal']) + answer = StrOption('answer', + f'Answer {idx}', + default=question['answer'], + properties=('frozen',)) + boolean = BoolOption('verif', + f'Verif of question {idx}', + Calculation(verif, + Params((ParamOption(choice), + ParamOption(answer)))), + properties=('frozen',)) + optiondescription = OptionDescription(f'question_{idx}', + f'Question {idx}', + [choice, answer, boolean]) + options_obj.append(optiondescription) + results_obj.append(ParamOption(boolean)) + + +options_obj.append(IntOption('res', + 'Quiz results', + Calculation(results, + Params(tuple(results_obj))))) +rootod = OptionDescription('root', '', options_obj) +meta_cfg = MetaConfig([], optiondescription=rootod, persistent=True, session_id="quiz") + + +def run_quiz(meta_cfg: MetaConfig): + pseudo = input("Enter a name: ") + try: + cfg = meta_cfg.config.new(pseudo, persistent=True) + except ConflictError: + print(f'Hey {pseudo} you already answered the questionnaire') + exit() + cfg.property.read_write() + + for idx, question in enumerate(cfg.option.list(type='optiondescription')): + question_id = question.option.doc() + question_obj = question.option('question') + question_doc = question_obj.option.doc() + print(f'{question_id}: {question_doc}') + print(*question_obj.value.list(), sep=" | ") + while True: + input_ans = input('Your answer: ') + try: + question_obj.value.set(input_ans) + except ValueError as err: + err.prefix = '' + print(err) + else: + break + if question.option('verif').value.get() is True: + print('Correct answer!') + else: + print("Wrong answer... the correct answer was:", question.option('answer').value.get()) + print('') + qno = idx + 1 + print("Correct answers:", cfg.option('res').value.get(), "out of", qno) + if cfg.option('res').value.get() == 0 : + print("Ouch... Maybe next time?") + elif cfg.option('res').value.get() == qno : + print("Wow, great job!") + + +def quiz_results(meta_cfg: MetaConfig): + for cfg in meta_cfg.config.list(): + print(f"==================== {cfg.config.name()} ==========================") + for idx, question in enumerate(cfg.option.list(type='optiondescription')): + if question.option('verif').value.get() is True: + answer = "correct answer" + else: + answer = "wrong answer: " + str(question.option('question').value.get()) + print(question.option.doc() + ': ' + answer) + qno = idx + 1 + print(f'{cfg.config.name()}\'s score: {cfg.option("res").value.get()} out of {qno}') + + +# reload old sessions +for session_id in list_sessions(): + # our meta config is just here to be a base, so we don't want its session id to be used + if session_id != "quiz": + meta_cfg.config.new(session_id, persistent=True) +while True: + who = input("Who are you? (a student | a teacher): ") + if who in ['a student', 'a teacher']: + break +if who == 'a student': + run_quiz(meta_cfg) +else: + quiz_results(meta_cfg) diff --git a/docs/src/validator.py b/docs/src/validator.py new file mode 100644 index 0000000..bb3c064 --- /dev/null +++ b/docs/src/validator.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 + +from tiramisu import StrOption, IntOption, OptionDescription, Config, \ + Calculation, Params, ParamOption, ParamSelfOption, ParamValue +from tiramisu.error import ValueWarning +import warnings +from re import match + + +# Creation differents function +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') + + +# Password must have at least min_len characters +def password_correct_len(min_len, recommand_len, password): + 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') + + +def user_not_in_password(login, password): + if login in password: + raise ValueError('the login must not be part of the password') + + +def password_match(password1, password2): + if password1 != password2: + raise ValueError("those passwords didn't match, try again") + + +# Create first option to ask user's login +login = StrOption('login', 'Login', properties=('mandatory',)) + +# Creation calculatin for first password +calc1 = Calculation(is_password_conform, + Params(ParamSelfOption())) + +calc2 = Calculation(password_correct_len, + Params((ParamValue(8), + ParamValue(12), + ParamSelfOption()))) + +calc3 = Calculation(user_not_in_password, + Params(kwargs={'login': ParamOption(login), + 'password': ParamSelfOption()}), + warnings_only=True) + + +# Create second option to ask user's password +password1 = StrOption('password1', + 'Password', + properties=('mandatory',), + validators=[calc1, calc2, calc3]) + +# Create third option to confirm user's password +password2 = StrOption('password2', + 'Confirm', + properties=('mandatory',), + validators=[Calculation(password_match, Params((ParamOption(password1), ParamSelfOption())))]) + +# Creation optiondescription and config +od = OptionDescription('password', 'Define your password', [password1, password2]) +root = OptionDescription('root', '', [login, od]) +config = Config(root) +config.property.read_write() + +# no number and no symbol (with prefix) +config.option('login').value.set('user') +try: + config.option('password.password1').value.set('aAbBc') +except ValueError as err: + print(f'Error: {err}') + +# no number and no symbol +config.option('login').value.set('user') +try: + config.option('password.password1').value.set('aAbBc') +except ValueError as err: + err.prefix = '' + print(f'Error: {err}') + +# too short password +config.option('login').value.set('user') +try: + config.option('password.password1').value.set('aZ$1') +except ValueError as err: + err.prefix = '' + print(f'Error: {err}') + +# warnings too short password +warnings.simplefilter('always', ValueWarning) +config.option('login').value.set('user') +with warnings.catch_warnings(record=True) as warn: + config.option('password.password1').value.set('aZ$1bN:2') + if warn: + warn[0].message.prefix = '' + print(f'Warning: {warn[0].message}') + password = config.option('password.password1').value.get() +print(f'The password is "{password}"') + +# password with login +warnings.simplefilter('always', ValueWarning) +config.option('login').value.set('user') +with warnings.catch_warnings(record=True) as warn: + config.option('password.password1').value.set('aZ$1bN:2u@1Bjuser') + if warn: + warn[0].message.prefix = '' + print(f'Warning: {warn[0].message}') + password = config.option('password.password1').value.get() +print(f'The password is "{password}"') + +# password1 not matching password2 +config.option('login').value.set('user') +config.option('password.password1').value.set('aZ$1bN:2u@1Bj') +try: + config.option('password.password2').value.set('aZ$1aaaa') +except ValueError as err: + err.prefix = '' + print(f'Error: {err}') + +# and finaly passwod match +config.option('login').value.set('user') +config.option('password.password1').value.set('aZ$1bN:2u@1Bj') +config.option('password.password2').value.set('aZ$1bN:2u@1Bj') +config.property.read_only() +user_login = config.option('login').value.get() +password = config.option('password.password2').value.get() +print(f'The password for "{user_login}" is "{password}"') diff --git a/docs/src/validator_follower.py b/docs/src/validator_follower.py new file mode 100644 index 0000000..6fa0a69 --- /dev/null +++ b/docs/src/validator_follower.py @@ -0,0 +1,41 @@ +from tiramisu import StrOption, IntOption, Leadership, OptionDescription, Config, \ + Calculation, Params, ParamSelfOption, ParamIndex +from tiramisu.error import ValueWarning +import warnings + + +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}%') + + +calculation = Calculation(valid_pourcent, Params((ParamSelfOption(whole=True), + ParamSelfOption(), + ParamIndex()))) + + +user = StrOption('user', 'User', multi=True) +percent = IntOption('percent', + 'Distribution', + multi=True, + validators=[calculation]) +od = Leadership('percent', 'Percent', [user, percent]) +config = Config(OptionDescription('root', 'root', [od])) + + +config.option('percent.user').value.set(['user1', 'user2']) +config.option('percent.percent', 0).value.set(20) + + +# too big +try: + config.option('percent.percent', 1).value.set(90) +except ValueError as err: + err.prefix = '' + print(f'Error: {err}') + +# correct +config.option('percent.percent', 1).value.set(80) diff --git a/docs/src/validator_multi.py b/docs/src/validator_multi.py new file mode 100644 index 0000000..8c87379 --- /dev/null +++ b/docs/src/validator_multi.py @@ -0,0 +1,44 @@ +from tiramisu import IntOption, OptionDescription, Config, \ + Calculation, Params, ParamSelfOption +from tiramisu.error import ValueWarning +import warnings + + +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%') + + +percent = IntOption('percent', + 'Percent', + multi=True, + validators=[Calculation(valid_pourcent, Params(ParamSelfOption()))]) +config = Config(OptionDescription('root', 'root', [percent])) + + +# too big +try: + config.option('percent').value.set([20, 90]) +except ValueError as err: + err.prefix = '' + print(f'Error: {err}') +percent_value = config.option('percent').value.get() +print(f'The value is "{percent_value}"') + +# too short +warnings.simplefilter('always', ValueWarning) +with warnings.catch_warnings(record=True) as warn: + config.option('percent').value.set([20, 70]) + if warn: + warn[0].message.prefix = '' + print(f'Warning: {warn[0].message}') + percent_value = config.option('percent').value.get() +print(f'The value is "{percent_value}"') + +# correct +config.option('percent').value.set([20, 80]) +percent_value = config.option('percent').value.get() +print(f'The value is "{percent_value}"') diff --git a/docs/storage.png b/docs/storage.png new file mode 100644 index 0000000000000000000000000000000000000000..9bef2b37a157f83657bad9613ee7f9f8df0dd38a GIT binary patch literal 15867 zcmeHu_dD0^|M%P8q0DUUY)O($5h*hob_yjU$|hS@%cxX#Mn+{sGK;2AMv~Q4A)>M} z@5gz4?&JFh+{gEO|8n0Q$8}uA`~7;Iuk(D4$9TSCj1KCuF|TK)P$+Es^>j=q6lzNf zg-Vi<9zPLLeVdH`(Vo-VZ^no}0gNZ2@oy$~y(8x+6s{)n9~HN|p%#9~@3q&`%hc_p z*9ALIN6Li@7o?rfo<8Sb=k6%&=6UMYPnGo)iYR5jj;5JU+Qety;|E7q)u*~YXPRf2 zTAo=R1VTZgEB8xrW_C(uF{kp-g*359^Bp>~bd{(*C2N4fTuDr=!o1`~u zvdy)7lD)3AN!3pB-p>y|{`#avwN{m!YhA3}amu>*+~;m%MlsrG{Lxc%DUc4qU-Am< z>c#QrDnAVmCnx8XH+-e!_a~Y|_Uh>9Y}`RFNq&1n&KAGqd>Ka-OTK)P)e^tdk$3oi z{;RneW7GwYhKKEsKRIvM>1bw9e!4p{KR>_VwWxUJc73sMjsoe->YFOQ@`sC$=a0!3 zY|_@&K3?O)Sy53@ARQ4MT_8GZn{RaHjO6;AF5xA$3;LZoW)dDA9+r>Y#m@HEyd=l) zHI&NW`B+yGi;azqd5Uq7&fdLtLyZvy(qZ_%>>AH4|83`2uvLuq>eZ`LpWNz(dLB5u zc=P7^vf$9r(EQS$caG}PoSf}Z2~>s5mtteJUnfi1xw$32ytXCl)Vn*f^74&uZtO6& zw4Cp1`Sj=%yYhvf$4@Ta4PJ?P>Oc3&Se}#9IH@tj{CReEcDnbEuD14eO;1mGJYmfJ z`;uq+YxpHBil1E;)9znLotqqJsXF)l)fskH)@OhJeE+)j7UERD-N>72V+sF`lN!g4 z2{5b?47qrbilSjYL)wLCYF6}UM2;^oVRpP!!?o0)}E z(a_g-c5+jux^05R#Ko!i?%iuy8?c*F-_fyld2vqnzya2^YuCzpjTntCMMOmO)cEd@ zwyt3e3=H&|>=&|naz4qr?!u(7mX?-mz`VPlsvlPryGUelvGl$K!I(%kfx{nKkP>Fa zJz?e%TwGkXEeS$=s(xi$#n#iK_wH(0Sn%4}*>zMLd34h5+nXCB{ZIG&9H>jYz2`zE z*4b0-EkO~LkYKs+vrlvXex~6sUpPfX5^wEtqo&MG4kWtO{@thC_5R+W_S!)IIQ|{< z{QUe%K2xIA-ajI-Lk6yAKUYUMzt6QB?|Hzt=e*>@hYzRTA5zaSFFdSw=#b34Ly_1) z`v(k#%vC|l$~eBEk&*e1?(*{TU+uagVsdiE&Wrh`_b#WVGVa*1gNvKnuBX&4RoQ#{ z!t`j^`H5b;k+uw_;1!ji`Jt%pJHa0xIx1cGDVUj+H8eiXDn`5V=kBU)Lns}elDp{o zU1ufB-@bgQdGzRdg`GREBqh-~I5?c?DWy91y)|%^JrwohIoTu?ziCE_b+5m9!QrB# zCQnR+_M8vDaf6?tp{-3r!>~qY-#&V2tLo{)-v^#tkh!_@H2tYlrzjy>8TF-hO-9F$ z$AvMlZ!c$KWDLp8-At)(YvX8bZPhb03@rSEN@aeS)zUn=s7Q)3I6LvFGC@>FFD>;&odNn)yk|%bPf>A|h|z zyy@=gNlmdlc5H);4EvEIM@m#HA3fS}@QzCUhjrZi{4trC;-eK}NG0!j&7bCP!kN~& zPYr5LkF?WZIoh8pTz^dsX)hsHAG;4xE1vCR+^}IoP9QT2OMBJ1Q%eJ@%OY!pl*3V? z?3d<$bv(Y0ozOmXi2X}TqApg~o~}vbvb#@z$h)W=wZ`t<9aTm)w!MG8y&W1IWjcNObeZ$}eJ-aQ9Ku+5 zrEL*=jI6BTwY6&0)YPR;?c(|I_&c_n*2>B%XMI{)TKc_v7HjS@VmZ%E8O6d2V)Kgo zK7Xd77#kaJFU&w#S=RW-H75uaK3u>3sF@iTic;K*7kZ-?G|<|%Bs7rLhQ@bz=P76B z7=&0sHLEzvc7#J|K~+zWrFn`*Qhny$*wWzDAk~Q4T%|P+L-c99q^$T9#_ibpm9>;!><^Y(qbO6pqRF?+*x2X>4jz z?^(znc6M^IeQ{aLvhu9Z;ro_ULPA10-Md_S!*S9LFJCfINLko(;b(n)J%yg0{$aPN zkx>W(yKsG9-%^q*$}PJX?STUa(v7yBD7hG2AdSwM$-Kk<8Ka8tZ`wWQ$BlaLBqv8U zMX@{l^Wq96{Y-(ysY6ya2@=sSx-c6vcOy>8G zJsciaSKA|d^*;r#{5|y{&zL4CD2Nh*nmaW7Jod%gx6eo4-?KwT;JRX4wtP=&!FeVZyPrGP)RAN0@x4_jP03^UY3oyuFZUVy%*=D-dns;*9+BipAw^JFkbxd;lrHr1uriJm+s>Dou|9-qpMffUcY`_6t&W# z#F~!cKb`j&5RZ&9g;^!20KdHv{O zqj2!@GO)9=f4F~ST`Z4OBW~?JJAMSEEi*lRu&CAC%q+WX<)4@9M#jc6t)KeI?%EX< z5kV&@CH3p&mbKJMW18ay&ebZ5`$?b{WHRqUjGg)T zkPfJ&9;tO{n`y3w;l$s%xi!Kn(ExsA58K|qr=g4w)QJFml&GF8I$G}jrI{_qHCQC| z=Rg01qdSt&&$Fppkx0zR$%(LQiX=_?F)+z-E2{^dU-j=>KV7ium*zCr{`aQcIR?Cz zM~>JbrU(o|8@ZrOZYt}To)h*MXJ9myCI&6p5Q@`TF(ju?}zwu31}7a}TsT zf=g?jJ~ajc3Yh(P%4c?*KUv!9QdyaTpo))C=ccus=DQJW1)=)-`Wyf#gM+r=QBfO! zfpCPWYJtk=+-)dw<|%qIfQ}*rUL%hRs=36kGWnNA7hHawBIAHYPl_x7vjahkY_+wu zN`BK@uU@+P-q+2iI~1fVQ0%*X{Uc%kCtRSSw=rNXz>+?}1B8JUjRpvGNW9y-2myLU+Bz#SF- zpFe+YFG`H=7a|wc0PsR$(Eu#4B3j?v+{uEv)Qls&f{Z~PrU$RCxCs{5F3%nVR7-Jd zH;XJU-%0*i<~AUVj@lG2uuB$e72I>46##!9@XZ6eCi;KAe=n>y6ZbP<77I7>=A0h7 zp&qP8&SqsMUx|*6uBXyXBs47S!Krt`D*m&q@#}YzC!uE-CdV(>N4C7TG(dq;NbDk3r@21SYDaP_;@yYw)NrHwj3&`))8M|5DeRE z#1#l>IuvO~R6y`opLymJjUdj#i#!)790LJX*$>kdV;x(2?b5 z|I=9TMt-Cet3~H^EQo~MvC6Y{KoF!M0{`0rulfG@CbnUNHiEF71)_!7)?K@J$qEyN zRgLO`0uS6)=0-U}O|C~ddT?RVdVX>7T+qg=Bcr2sfWOhPu}$dkgTuq25iGoouU;{O zM}p(j6c}t2pW+cSj*yU+ZqCq-Ld}W8DT?FrXf3I#{)!~1OYO9ABqEduEviF@4god~ zBS1*wQv@DCy{muwmJOTO9708tj$S)vB9R2#|IfE?pQR2&&Z)2B}Y*c;O1{dx0_ zGm1^Mhy;sn-3r{z+1Ak!diSnK?tvSjJW@yVs%z$cqg8I)yg8Z41eBr)K*gr&ZbHJr z8+Y!d%VnUdgN)cV|G2yaGa=0=&irO>{<}| z-5c|dpFFv_tHdS|m~g|UO>}5)sY)K=YRl86AM#CgJHKw*?fDO5h3ltCl;L4Og^u%5 zQc^viK4oDWOcD=`+MhXdzwy#QKYHow@ZyfI&u=Ui6%~Oa#X!YKM=I2m&r{j`XP=f7 z-vB+6(;|=?0B`0;cT`=SItkMIM|L1c&GvbC$eo|;k55k*g>!4OqP4fT z&;Bv4D-sxv+uJ{j=7a=f;*m3bh_BRatGc?#sn#@OZ~n;>z_vt75#O-E-0IaHxg9&g zva-JHnOGz;3~CC(^Re%r&8)1fQzgO6ELfQPg$pH9$2fH+w<{|0XliPbn(+J2pLW2R z12^R9u;ImRbuEP!CCzBO*>YxQ&z|Mmagr6Ni^l)WWjfZU!JF6bgel4Wd)$4m;6Qd9X3+)`vi zd%?2@J3awIgN{Sf(9ocSI6-S|gKmLxYdctfQKT=GJu}z@f)+XvNy06M?huNMu#Vh+ zZ8N^3Is2#T$&)Wg*U|?MH1+ft`}+D$sOy?@2??=5K-{=#Q$uIxOxV+PCY^GS9!FRE zvNAKxj_P`^3uHS{h}g|zrP*%`1oEJdCy$Ac8!LYmlw5R z7}6pHrLY2n%(@|4PH_qZ#OILjY|oxe=zr=@NYecD`$Hap;pEeWCAIdCGhLoPe@+KM ztoiM&U2?%I0mo|t_*MO9!?UtBfT?VH>$y_AvN*6xO?(utjvF5hv4_(h-bNxrDb%MZX&gh8UvrX8mpz5 zE1R$i-|EU-ot>*|JQfZ;h6WKo^6}9Y1%>(4gPFX}E-v=iRVDvf$%A**;sI#wX2v?5 zdP>EJ^ahdSY3DB@{SN+pzoxg$IqdP{$G^8e(bqq4KnF-8XLsXz1*i1kOCyearFp$K zW$mbm5{R;5j}6W{@+h~!Kzu>;aaEPr#*MVwwrwNr3+$T)gh#=-gBJZeaAL!0{iHRk zt{vG-(l)hmcuu+eS4z`wU-m|gD{{Bnz1Eu(eRibyBqME2_^X6 zn0&OF<+pF&h-3}sSqfh0#H zO;KM!j+M@j^HrapxU{-F8H}iGs#}?nalYRctM6Z`prBCdKBR>u(BQ5YV`7*{{Zk8E zNI!U6d88wU1&g?^kmYO63kbi-Aeq$EJkxt?{N{ef6G;}Ig7owZrSyT%FZ*qlP)nQ=hUv5FWy=pDNmNwo{kVcFez4PcTmSs+yAhl6wl(ckogzrIb+>GiNhHs|) zv;gj!|NFP$-8*)q5K(cVdJ)|)P1T>LB~hg1*O!*0Sz`-}C}^0|gWq|ZEP%Boh9_!G z{QRm-LbfmO0H%Xn?$~=8q!|2uYHBLv-n}gd$3L%RK7)c&Y}pSxKL6LMAg~fC(2Vj! zU?lc0L7u;?*0H0~1 zP93#AzIyL2mFR!LCEp)+V`F1&7Wzp#Ffc^P#}*Y$2n5IeHB{3!A+$@pfWxZVOA|zf zFlVBQ0tdO3*F{Fsk55bxvjW9M;%RbH(oDx7DFgrw+4t_f{(E@kN^&v})ZYlyj;9KG zodxDUp1x~17M>(POfmDV2v-HyUM^hj;Y=;x`t_M;_oToL72x}DwME6aP-X|%RdPtXK-SI4eEPpYN`g#1&pGRU41E}v9Zx}_g!f1&50t3 zgmJNps6E4_r|MRh56zzAG@jw)=VyZ4J~A=_?d*7Uvg_w+a~GteaKP&F-1hqq13WJK ze{B}N@3IlPwVIlm6N(1`JiszvR+bl)f|iu1X=sMP_krz?yyF)ZUIPJ)$T=Mqkd%IZ zbkOPSTkPxC-@jiI0@2|3IDy!i|FJ7tfZ4K72~yVse7mnK`2y8iS5!-^o3%AR%WB!F zgMGk#7|=@ucYErrXCYWT0D3c@ve#BGZ||I0C}KnaU%PIdougv}0QuIpb)J*`s?aJZ zpk@d;YFHOUR09UncO?vHu%0UQ#tpH}oA>n=B&cG0&-hls;J#p%LEJvwtY}6VZ8YhAqq=YNCZb?`LlVAlhr@V`jPE!vS z#yLj8l_0Fg(9qC%6`G4E*z)Y`>|?)~?Zj}Bk&%H=NPrx=;$D~__wU~y92;X661pw# zy?NutK~z4{9*&kdK@&5BPL=F?eh>`R9r^}hvk}cbo}vC6>)}mX^c*4RZp43PV&9pW zm^djqCrJdd09h#0Z^v^#V(dIXoA1QOXQu^@WT7Y(5m=TR{-W;HC zfH+?6Mf96N++=|pHZ(XWDg|{lQ#VoAgg}*cphPMSB_$&EXzDviMln9lEi82iM>-KaKy3TCkV$kwTcP4aBEo4{qY{23~r4}RUA3C=bU9T5= zxb~KDLOr=Ui}q~4s|++iRm_(Dw(d#3KL zE-?%Qud2dNq^L|bjP+IR0Gj3y5s{IC*JrNW3hBZ9+Z%ouOnR=f;~zJ?-?JsNAeT0l z-XB`x88Qn%ELAzVi&0Sw!0{aG*I$Z@W6e=S1~$VZZF}fwFj~EqkB^ZuKmO<)5%N%S z#Zje6IR`+N_5FP!$KlztXX)A5r0Ww0r?=9L9XbL4mAIT7CyHbvK4^RLWF!Pn{l(?K z-|rGT<6Gy099~{wAwhy1 zTB6d@KZ{%zAs|B}lnyLxYi(TvU&61@fx~zvBR@Z;Ze@Yx;>C;6mo9~Z+`M`Fw*9I9 z1&Bd4=Ldko?6E>(J}RFdXQVg{)T%;nAfCdBlP9;A--XNqb(*45$9>A)o*I0BFd5Kw zvSRcxH2Y0&-_$z3=0SnB#WA;abw%E}Ba8}`6ndHCUkM?abHgB7*Wh0V5B%8UN0(F@ z(1eIO1L+d$;UL)oy8?Sb+c7n zkX7$V2L~D?PR{v{A3sjb%tU|!2nq=eO-#fSV+Fkx!tfoh1faQX%V8CGM5nI%H_GnZ zym<))eZ04v1!4*@G265EuY=mJfoe2*ba8uVSf9ksHJ&A#*DIdEs0a+$aHQzdhgPF$ zmbjDtCxdRAv>gTse;=WlXA~C3KDjWt-F09VqdD>_NF+avHTt`q3&#G!bR6jq48{Jc#zy@J@X-4!C@O<+w}N{l6Wj zdtyK7Gkjo&5N&O3?bzGf(^E4WNQ3S+&gWkkgM!k~*C+Vr`@7)Lx}SGcd_z%+EiElI zU}!>^&@a)|(b32-swae~H|_RqdT-g8kN?WSo}LfOq@<kBsH(PH3Uh|0^a#ZxJr`^HL^ zj_*3)Fh~~xey2k%BZLsD!*f*oUnA`sh<^^ZEDTx;Aomu3SCR`cF%6KHi0%sOaR`3h zm1PwjGvDh6_sY8UtDG6AT`rondg`|m85aesc@Vf+58@7SYfrviU*`Jh@J;&WrY0J= zC(kGARuho;DnUzpSl%;~HR1pRPYwYdT9&(Tlfj3xeM&i_F4oQO-><_KLqXF%e3&O$ z@<=F5)f`0_NW{R0Ud)Mt1&B0s?U35V2M->YJb}VC3_?lV1dKKqo0x>6bxB%Pn>nkM z)V+qRnJVXy&2UVBpZ{s+cU^8Ba~qrNOSf|~KH!TwcnWLy3$!dHvSCdgp zOIv&Iwatd%a3@E4%L{=!ODLkSK%@t5t*$B=(p(vv8GWxy;b4x?qHv%Ltwox;l}!Sd zgad+>xs7@(%uYxk&vVWr)9g_)GI0XbA*qV4H#xKjCXn;|M=!%@YdE6FXe$?yN1*#z z^j-j|Yy9}J>P?l~K&=$uIgt(8K78N;G+V2_JbjHKiVm249!M_JAVs>dzFtGYt^YU( zXe^w_7(gd|SLgxH5Caid{C~s>tqhLh;^JY5434crKSWY44l-{rh(qxyFSVl9DrjrbkUZzP{py2V#CPNwv~zv_qa(vP#yLGvC^| zo|&2X>bG`8^*SPQ;E$VE9G;Gomv>9C_0s~bgnqMw2NNE9e!cXfwKeER;3TY~j`!~e z9tR>dD55ZFjlY#cU%Qf)CW+20F03AO{M!ubWHgE!GXn#|HX}bDpXkZj#e)GD7af_wL=BHvCqZbMM|{EC7QGdcX#)YurTc$VSg{kA8~@a*W$Q3b_vZ*e6lzxcz9GNmxz>l zdi7slr;w7?SM6=!l6CK1)Rhgo_QTI&+|Qps+{-(tz=qKkp8PcM8scoh9!6qhqPEo7 za+qs_afYCaf~*o%3RVsWq*5a40mMXMOlMp9E_wN@ja{*^w{PE;eE*^*5qEPK?a1*V z{Dh6|N=iypcsMP_wvI5HlJ)58Hv*enpEcjg<E`db!xkqdMjSC?tCJdEBGAbe=9J=ecvxmo`YPmfg918yNqrsF)ZP2FZd)7Zu=CB5O@F zyw08T&pMqksFQ_`O?aIP&R6PanFjFV^@@~(r>E=_+hT`)7@pJ)<_0|@Zhl@KBf3_H zgM0s8r0DgtuCa85_eGnMMAtH*mR|o14FOynYs9UPVSTVOC7Kr|0C*Nk|lW?mq9>p!qXNAXowb zQv~ycgf?L`ZHRA8zX(H>#o47I4h$xR@R>;LBD6V^oudVxSt>FrH(w}G7ccM`5A+RQ zVkN&Vs32|-An_M?|72isq)6`H>zTRkJk!Gg0T2PV4<&MHGI(|I>E_KFGj~}idu(ZH zc=APb?p1spcR>Z24jUwPPhF5IfZGGx25O>pqd{0=KH_IzRo2+rn1VC{yYedxIhzW$*GfD``mmm)h4~n-0p`8dSZVUbCp;s7WaWTg(k%nlwbnPx+0C$n@LR8@t5 zrZL314dyQ~NpX1j)n=LcgmYsSl26u_0#+9bb}9~qnwUKJ8c}$N+X6)v0aGA77k2IX z^>8o;f_FzgRhAUI&PW-|@-&kKv%`E+fppeURQbOxiXws2oCJ2mp%CA^nI7^WG}R`o zCcUJD9qt{`ZqRVouU~o>&FG_6yd9Jqql7(G=e9h2^vLeanK<(Vbd(ItNiNJy%4B9{ZZBP6Z$Yupad$6u_x9$dgrk4$ zIMvDp2ed#s-daME9N^>tw+|gImCT;2T>+ozL_L zCnX&$2VniNx6phW$qC%t&DV<97R&} zV9*t~h|GsU0ZvExo|>DB0!AAjA2+iSzaj~{m5?_q2(r5-1{+~&610x_1A=i|TUs$! z1%nS$c4XvD?^6-T{?HJy6frH+^z4~VLZ!(zCb7$DX>nk$Uj`e(UL+R20TlURWFW!oSqC zw-*P+y`GF^f>rBJl$DiDL2_?Gv|@lW9C9LaVQwXKL9>GZ1q4rqhldY>IFq@c@2$6o zCMOf277=Z|K>Er^4h|)Zc*a0BL=XR$cu)QL)0D0m25Z3DuDq`xWG}{0G zL6Y;m;^H*|0=Hlxf)KQIcSnH;K?HjJ^!q&&RH-CR~9{kTRC^_uxOjNOiRB}ZkpZPP^dd3t(g z4^9vNo^zF936}9Q=kRiOw`IS;4FF-*00n4kyhD}u4<5t^6UqL?#VnM)wAqW>58%rn zV-biZK_zzz#KzL{@(9E~$ZmU?GxB-V=PfM;m3^qU7MoeW!fysjuK2T^iy2~G zjY8fLOUr=LO8L=cg7Bg_sPXjVX^9yLL8sx_Gg>0+lZ-01+M`Atc47`aZTwRuI}U|} zJ0wpdOoOnlS71aNMgp6A$8cxDl{hfscZ35~MPl7e{otTqhgK@}gIn-OOz3gL#7siG zp{QNHb&CZugmZ5c0BwG?&O!(`FE4R?$nOC^fI}l7SlQNxP+>#&-aB}EzDVKU_YchF zfvLzPNJc?l%PK83TF>(3`*#K)w|~8OFk1zpLAXRsoiyF)phK`vd`lPu% z4zCgQi4YVR~g10A+rlFk-Wk_+%}?E$7F3E&`u? zD7F$n!XRewbc+J<85=TRMd2drFmcAv|~Vt93PZ` zfWtz@+$i#9hDhPTSee>-UYmf1?|J#4IdkcSg_xC%*4roe&q1ARdsYh_bN2s(_j$hP zWa(@a5Thk#8CvonC?#>lAT?8w3CVoZ@9~MIAn({6V0j5w8ENT#@F|cj*DKz^(jlrM zgaql=i=yIw@FB#-kH8(O$NUHctftz)MKYj^mJ%ndx}Lap17nuZ^(4H!yk4EL0TxtS z7|D|V#Ky{sX_x8N@^zdq<8g2sWATLl&Y8obkoB8(a$bZ&jdYywRGph1egAr9UPs3* z_TNte`BNysZPzP)jEyz+_ggz3LzjwV3Y}gm4f!QZAjRi6SK1pBLK7(J#1K3=vWDOZ=cC5Q(jARs{Id?D<~2fFB(nWGk6ktDz*LhB+pKEMGa zHjHN-tvC|{OTZS^8u5tnx&;Fo01XAxrlcNILjF_jfkLTZtdu#mQ$rZ(m?fD(A=8Ke zuKM4J+FRi^kU;EC^b3d*>(LDuU@ZLGr6buz5@57sfP3=o_S{II2gD1!5~JnvC}`PF z(z=EZV-f!jaABYT6UGKz?-KBylBQ0lFh$O-zh+LY72`Dj(H}5F^lbjum**gC*LyK5 zVoc-;=T89Ov=rj~;{_-*j$y}?dvIBSYh&d zi6~1z8q`E`a%wB{4x@*yan@)GF&JpF2jal!XbXzmjT5U@K8OlD|s1-02YwD==k{O@VBnd{s3WtKz^by3kM@BD|@*M1UCa( z2e=@m9z_|0+(2+5HjIq9!_jU4Bsx);6kQ;UNloJ_ zIgG5J5J9bhKEDPq8QGZy9^V5q{RZCDprTNAmZ#X<LuGhV+ydDq6PSN;9{ zWViRiS1B^!p=kXA8#oeDj3xTc90~zLAoFr|-%d5=f2`|I* zPrbdvcuQ%owe=kn8D7rzfnPjgoGu>oWns1$;@wviEsg2Ek}vqa;8 zT+#rFN5(x73(%M1FbqQG9Fns>P#zw zcIWjzEGv74vog1$=QP($%Z0M`Kk-uOeCa81Y6?su(e2yy!6d=#wiQTUjD8h^GGk^% z%bA~y!Cjn~Io{wo;pq50eXkiEky9{l3Ae6 jU%mam_nPseRfY|IRWB48{1Wk^Hf8_bgF3}pwxRzE=}D9J literal 0 HcmV?d00001 diff --git a/docs/storage.svg b/docs/storage.svg new file mode 100644 index 0000000..d710cbc --- /dev/null +++ b/docs/storage.svg @@ -0,0 +1,265 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + Config + + + + + Values + + + + Settings + + + + + + + + + + + + Storage + + + Option + + + + diff --git a/docs/symlinkoption.rst b/docs/symlinkoption.rst new file mode 100644 index 0000000..40e0081 --- /dev/null +++ b/docs/symlinkoption.rst @@ -0,0 +1,13 @@ +==================================================== +The symbolic link option: :class:`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: + +>>> from tiramisu import StrOption, SymLinkOption +>>> st = StrOption('str', 'str') +>>> sym = SymLinkOption('sym', st) diff --git a/docs/validator.rst b/docs/validator.rst new file mode 100644 index 0000000..70b0345 --- /dev/null +++ b/docs/validator.rst @@ -0,0 +1,271 @@ +================================== +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 :doc:`calculation` 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: + +.. literalinclude:: src/validator.py + :lines: 3-7 + :linenos: + +Create a first function to valid that the password is not weak: + +.. literalinclude:: src/validator.py + :lines: 10-14 + :linenos: + +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: + +.. literalinclude:: src/validator.py + :lines: 17-23 + :linenos: + +Thirdly create a function that verify that the login name is not a part of password (password `foo2aZ$` if not valid for user `foo`): + +.. literalinclude:: src/validator.py + :lines: 26-28 + :linenos: + +Now we can creation an option to ask user login: + +.. literalinclude:: src/validator.py + :lines: 36-37 + :linenos: + +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: + +.. literalinclude:: src/validator.py + :lines: 39-41 + :linenos: + +Create a second calculation to launch `password_correct_len` function. We want set 8 as `min_len` value and 12 as `recommand_len` value: + +.. literalinclude:: src/validator.py + :lines: 43-46 + :linenos: + +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: + +.. literalinclude:: src/validator.py + :lines: 48-51 + :linenos: + +So now we can create first password option that use those calculations: + +.. literalinclude:: src/validator.py + :lines: 54-58 + :linenos: + +A new function is created to conform that password1 and password2 match: + +.. literalinclude:: src/validator.py + :lines: 31-33 + :linenos: + +And now we can create second password option that use this function: + +.. literalinclude:: src/validator.py + :lines: 60-64 + :linenos: + +Finally we create optiondescription and config: + +.. literalinclude:: src/validator.py + :lines: 66-70 + :linenos: + +Now we can test this `Config`: + +.. literalinclude:: src/validator.py + :lines: 72-77 + :linenos: + +The tested password is too weak, so value is not set. +The error is: `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: + +.. literalinclude:: src/validator.py + :lines: 79-85 + :linenos: + +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: + +.. literalinclude:: src/validator.py + :lines: 87-93 + :linenos: + +The error is: `Error: use 8 characters or more for your password`. + +Now try a password with 8 characters: + +.. literalinclude:: src/validator.py + :lines: 95-104 + :linenos: + +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: + +.. literalinclude:: src/validator.py + :lines: 106-115 + :linenos: + +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: + +.. literalinclude:: src/validator.py + :lines: 117-124 + :linenos: + +An error is displayed: `Error: those passwords didn't match, try again`. + +Finally try a valid password: + +.. literalinclude:: src/validator.py + :lines: 126-133 + :linenos: + +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: + +.. literalinclude:: src/validator_multi.py + :lines: 1-4 + :linenos: + +Continue by writing the validation function: + +.. literalinclude:: src/validator_multi.py + :lines: 7-12 + :linenos: + +And create a simple config: + +.. literalinclude:: src/validator_multi.py + :lines: 15-19 + :linenos: + +Now try with bigger sum: + +.. literalinclude:: src/validator_multi.py + :lines: 22-29 + :linenos: + +The result is: + +`Error: the total 110% is bigger than 100%` + +`The value is "[]"` + +Let's try with lower sum: + +.. literalinclude:: src/validator_multi.py + :lines: 31-39 + :linenos: + +The result is: + +`Warning: the total 90% is lower than 100%` + +`The value is "[20, 70]"` + +Finally with correct value: + +.. literalinclude:: src/validator_multi.py + :lines: 41-44 + :linenos: + +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: + +.. literalinclude:: src/validator_follower.py + :lines: 1-4 + :linenos: + +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 + +.. literalinclude:: src/validator_follower.py + :lines: 7-12 + :linenos: + +Continue by creating a calculation: + +.. literalinclude:: src/validator_follower.py + :lines: 15-17 + :linenos: + +And instanciate differents option and config: + +.. literalinclude:: src/validator_follower.py + :lines: 20-26 + :linenos: + +Add two value to the leader: + +.. literalinclude:: src/validator_follower.py + :lines: 29 + :linenos: + +The user user1 will have 20%: + +.. literalinclude:: src/validator_follower.py + :lines: 30 + :linenos: + +If we try to set 90% to user2: + +.. literalinclude:: src/validator_follower.py + :lines: 33-38 + :linenos: + +This error occured: `Error: the value 90 (at index 1) is too big, the total is 110%` + +No problem with 80%: + +.. literalinclude:: src/validator_follower.py + :lines: 40-41 + :linenos: