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 0000000..e3df4de Binary files /dev/null and b/docs/_static/python-logo-large.png differ 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 0000000..4a6fcc2 Binary files /dev/null and b/docs/images/firefox_preferences.png differ diff --git a/doc/README.md b/docs/index.rst similarity index 64% rename from doc/README.md rename to docs/index.rst index 3f93181..de63629 100644 --- a/doc/README.md +++ b/docs/index.rst @@ -1,36 +1,49 @@ -![Logo Tiramisu](../logo.png "logo Tiramisu") +.. default-role:: literal -# Python3 Tiramisu library user documentation +.. meta:: -## The tasting of `Tiramisu` --- `user documentation` + :description: python tiramisu library user documentation + :keywords: python, tiramisu, tutorial -Tiramisu: +.. title:: Tiramisu -- is a cool, refreshing Italian dessert, -- it is also an [options controller tool](http://en.wikipedia.org/wiki/Configuration_management#Overview) +The tasting of `Tiramisu` --- `user documentation` +=================================================== -It's a pretty small, local (that is, straight on the operating system) options handler and controller. +.. image:: logo.png + :height: 150px + +`Tiramisu` + + - is a cool, refreshing Italian dessert, + + - it is also an `options controller tool`_. + +.. _`options controller tool`: http://en.wikipedia.org/wiki/Configuration_management#Overview + + +It's a pretty small, local (that is, straight on the operating system) options +handler and controller. -- [Getting started](gettingstarted.md) -- [The Config](config.md) -- [Browse the Config](browse.md) -- [Manage values](api_value.md) .. toctree:: :maxdepth: 2 + gettingstarted + config + browse + api_value api_property - storage application quiz glossary -External project: - -.. toctree:: - :maxdepth: 2 - - cmdline_parser +.. External project: +.. +.. .. toctree:: +.. :maxdepth: 2 +.. +.. cmdline_parser .. FIXME ca veut rien dire : "AssertionError: type invalide pour des propriétés pour protocols, doit être un frozenset" 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 0000000..084a534 Binary files /dev/null and b/docs/logo.png differ diff --git a/docs/option.rst b/docs/option.rst new file mode 100644 index 0000000..0ebb5be --- /dev/null +++ b/docs/option.rst @@ -0,0 +1,136 @@ +================================== +Instanciate an option +================================== + +Option +======== + +.. 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. + + * - multi + - There are cases where it can be interesting to have a list of values rather than just one. + + * - default + - 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. + + The default value can be a :doc:`calculation`. + + * - 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. + + The default_multi value can be a :doc:`calculation`. + + * - validators + - A list of :doc:`validator`. + + * - warnings_only + - Only emit warnings if not type validation is invalid. + + * - properties + - A list of :doc:`property` (inside a frozenset(). + + +Examples +========== + +Let's try a simple option: + +>>> 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 0000000..9bef2b3 Binary files /dev/null and b/docs/storage.png differ 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: