feat: documentation

This commit is contained in:
egarette@silique.fr 2023-12-17 21:22:52 +01:00
parent 428e243630
commit 93fa26f8df
69 changed files with 5335 additions and 2179 deletions

32
.readthedocs.yaml Normal file
View file

@ -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

View file

@ -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:
```
[<tiramisu.api.TiramisuOption object at 0x7f490a530f98>, <tiramisu.api.TiramisuOption object at 0x7f490a530748>]
```
If the option name is unique, the search can be stopped once one matched option has been found:
```python
async def main():
cfg = await Config(rootod)
print(await cfg.option.find(name='var1', first=True))
run(main())
```
returns:
```
<tiramisu.api.TiramisuOption object at 0x7ff27fc93c70>
```
Search object behaves like a cfg object, for example:
```python
async def main():
cfg = await Config(rootod)
option = await cfg.option.find(name='var1', first=True)
print(await option.option.name())
print(await option.option.doc())
run(main())
```
returns:
```
var1
var1
```
Search can be made with various criteria:
```python
async def main():
cfg = await Config(rootod)
await cfg.option.find(name='var3', value=undefined)
await cfg.option.find(name='var3', type=StrOption)
run(main())
```
The find method can be used in subconfigs:
```python
async def main():
cfg = await Config(rootod)
print(await cfg.option('od2').find('var1'))
run(main())
```
## The "dict" flattening utility
In a config or a subconfig, you can print a dict-like representation
In a "fullpath" or a "flatten" way
- get the "od1" option description:
```python
async def main():
cfg = await Config(rootod)
print(await cfg.option('od1').value.dict(fullpath=True))
run(main())
```
returns:
```
{'od1.var1': None, 'od1.var2': None, 'od1.var3': None}
```
```python
async def main():
cfg = await Config(rootod)
print(await cfg.option('od1').value.dict(fullpath=False))
run(main())
>>> print(cfg.option('od1').value.dict(fullpath=True))
```
returns:
```
{'var1': None, 'var2': None, 'var3': None}
```
> **_NOTE_** be carefull with this "flatten" parameter, because we can just loose some options if there are same name (some option can be overriden).

View file

@ -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)

View file

@ -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).

View file

@ -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).

View file

@ -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)

View file

@ -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).

View file

@ -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')
```

View file

@ -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)
```

View file

@ -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.

View file

@ -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)
```

View file

@ -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())
```

23
docs/Makefile Normal file
View file

@ -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

4
docs/_static/css/custom.css vendored Normal file
View file

@ -0,0 +1,4 @@
.wy-table-responsive table td {
white-space: normal;
}

BIN
docs/_static/python-logo-large.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -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 <src/api_global_permissive.py>`
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.

View file

@ -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 <src/api_global_property.py>`
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.

View file

@ -0,0 +1,17 @@
=====================
Option's permissive
=====================
.. FIXME advanced in permissive
.. FIXME unrestraint, forcepermissive

View file

@ -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 <src/api_option_property.py>`
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.

31
docs/api_property.rst Normal file
View file

@ -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"""

View file

@ -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: 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: Let's import needed object:
```python .. literalinclude:: src/api_value.py
from asyncio import run :lines: 1-4
from shutil import disk_usage :linenos:
from os.path import isdir
from tiramisu import FilenameOption, FloatOption, OptionDescription, Config, \
Calculation, Params, ParamValue, ParamOption, ParamSelfOption
```
Create a function that verify the path exists in current system: Create a function that verify the path exists in current system:
```python .. literalinclude:: src/api_value.py
def valid_is_dir(path): :lines: 6-9
# verify if path is a directory :linenos:
if not isdir(path):
raise ValueError('this directory does not exist')
```
Use this function as a :doc:`validator` in a new option call `path`: Use this function as a :doc:`validator` in a new option call `path`:
```python .. literalinclude:: src/api_value.py
filename = FilenameOption('path', 'Path', validators=[Calculation(valid_is_dir, :lines: 24-25
Params(ParamSelfOption()))]) :linenos:
```
Create a second function that calculate the disk usage: Create a second function that calculate the disk usage:
```python .. literalinclude:: src/api_value.py
def calc_disk_usage(path, size='bytes'): :lines: 11-21
# do not calc if path is None :linenos:
if path is None:
return None
if size == 'bytes':
div = 1
else:
# bytes to gigabytes
div = 1024 * 1024 * 1024
return disk_usage(path).free / div
```
Add a new option call `usage` that use this function with first argument the option `path` created before: Add a new option call `usage` that use this function with first argument the option `path` created before:
```python .. literalinclude:: src/api_value.py
usage = FloatOption('usage', 'Disk usage', Calculation(calc_disk_usage, :lines: 26-27
Params(ParamOption(filename)))) :linenos:
```
Finally add those options in option description and a Config: Finally add those options in option description and a Config:
```python .. literalinclude:: src/api_value.py
disk = OptionDescription('disk', 'Verify disk usage', [filename, usage]) :lines: 28-31
root = OptionDescription('root', 'root', [disk]) :linenos:
async def main():
config = await Config(root)
await config.property.read_write()
return config
config = run(main()) :download:`download the config <src/api_value.py>`
```
#### Get and set a value Get and set a value
'''''''''''''''''''''''''''''
First of all, retrieve the values of both options: First of all, retrieve the values of both options:
```python >>> config.option('disk.path').value.get()
async def main():
print(await config.option('disk.path').value.get())
print(await config.option('disk.usage').value.get())
run(main())
```
returns:
```
None None
>>> config.option('disk.usage').value.get()
None None
```
Enter a value of the `path` option: Enter a value of the `path` option:
```python >>> config.option('disk.path').value.set('/')
async def main():
await config.option('disk.path').value.set('/')
print(await config.option('disk.path').value.get())
print(await config.option('disk.usage').value.get())
run(main()) 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 668520882176.0
```
When you enter a value it is validated: When you enter a value it is validated:
```python >>> try:
async def main(): >>> config.option('disk.path').value.set('/unknown')
try: >>> except ValueError as err:
await config.option('disk.path').value.set('/unknown') >>> print(err)
except ValueError as err:
print(err)
run(main())
```
returns:
```
"/unknown" is an invalid file name for "Path", this directory does not exist "/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: To check is a value is valid:
```python >>> config.option('disk.path').value.valid()
await 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: 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() >>> config.option('disk.usage').value.default()
668510105600.0 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: 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() >>> config.option('disk.path').value.get()
None 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. 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() >>> config.option('disk.path').owner.get()
itsme 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`. 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: 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 :lines: 26-31
:linenos: :linenos:
We set the default value to `bytes`, if not, the default value will be None. 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 <src/api_value_choice.py>`
At any time, we can get all de choices avalaible for an option: At any time, we can get all de choices avalaible for an option:
>>> config.option('disk.size_type').value.list() >>> config.option('disk.size_type').value.list()
('bytes', 'giga bytes') ('bytes', 'giga bytes')
### Value in multi option Value in multi option
--------------------------------------
.. FIXME undefined .. FIXME undefined
@ -237,25 +208,26 @@ First of all, we have to modification in this option:
- add multi attribute to True - 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 - 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 :lines: 23-25
:linenos: :linenos:
Secondly, the function calc_disk_usage must return a list: 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 :lines: 11-26
:linenos: :linenos:
Finally `usage` option is also a multi: Finally `usage` option is also a multi:
.. literalinclude:: ../src/api_value_multi.py .. literalinclude:: src/api_value_multi.py
:lines: 27-30 :lines: 27-30
:linenos: :linenos:
:download:`download the config <../src/api_value_multi.py>` :download:`download the config <src/api_value_multi.py>`
#### Get or set a multi value Get or set a multi value
'''''''''''''''''''''''''''''
Since the options are multi, the default value is a list: 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() >>> config.option('disk.usage').value.get()
[668499898368.0, 8279277568.0] [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: 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() >>> config.option('disk.path').owner.get()
user user
### Leadership Leadership
--------------------------------------
In previous example, we cannot define different `size_type` for each path. If you want do this, you need a 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: So let's change this function:
.. literalinclude:: ../src/api_value_leader.py .. literalinclude:: src/api_value_leader.py
:lines: 12-18 :lines: 12-18
:linenos: :linenos:
Secondly the option `size_type` became a multi: Secondly the option `size_type` became a multi:
.. literalinclude:: ../src/api_value_leader.py .. literalinclude:: src/api_value_leader.py
:lines: 24-25 :lines: 24-25
:linenos: :linenos:
Finally disk has to be a leadership: Finally disk has to be a leadership:
.. literalinclude:: ../src/api_value_leader.py .. literalinclude:: src/api_value_leader.py
:lines: 30 :lines: 30
:linenos: :linenos:
#### Get and set a leader Get and set a leader
'''''''''''''''''''''''''''''
A leader is, in fact, a multi option: A leader is, in fact, a multi option:
@ -346,7 +321,8 @@ To reduce use the `pop` method:
>>> config.option('disk.path').value.get() >>> 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: 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() >>> config.option('disk.size_type').value.len()
2 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: 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() >>> config.option('disk.size_type', 1).owner.get()
default 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: 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.path').value.set(['/', '/tmp'])
>>> config.option('disk.size_type', 0).value.set('giga bytes') >>> config.option('disk.size_type', 0).value.set('giga bytes')
>>> config.option('disk').value.dict() >>> config.option('disk').value.get()
{'path': ['/', '/tmp'], 'size_type': ['giga bytes', 'bytes'], 'usage': [622.578239440918, 8279273472.0]}
An attribute fullpath permit to have fullpath of child option:
>>> config.option('disk').value.dict(fullpath=True)
{'disk.path': ['/', '/tmp'], 'disk.size_type': ['giga bytes', 'bytes'], 'disk.usage': [622.578239440918, 8279273472.0]} {'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: 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.path').value.set(['/', '/tmp'])
>>> config.option('disk.size_type', 0).value.set('giga bytes') >>> 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]} {'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: 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]} {'path': ['/', '/tmp'], 'size_type': ['giga bytes', 'bytes'], 'usage': [622.578239440918, 8279273472.0]}
### importation/exportation importation/exportation
------------------------
In config, we can export full values: In config, we can export full values:
@ -439,4 +415,3 @@ and reimport it later:
>>> config.value.importation(export) >>> 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. .. note:: The exportation format is not stable and can be change later, please do not use importation otherwise than jointly with exportation.

119
docs/application.rst Normal file
View file

@ -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
<https://www.mozilla.org/en-US/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 <api_value>`, :doc:`property <api_option_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 <src/application.py>` 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"

159
docs/browse.rst Normal file
View file

@ -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 <src/property.py>`
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'))
<tiramisu.api.TiramisuOption object at 0x7f3876cc5940>
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'))
# [<tiramisu.api.TiramisuOption object at 0x7f490a530f98>, <tiramisu.api.TiramisuOption object at 0x7f490a530748>]
# If the option name is unique, the search can be stopped once one matched option
# has been found:
print(cfg.option.find(name='var1', first=True))
# <tiramisu.api.TiramisuOption object at 0x7f6c2beae128>
# 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 <src/find.py>`
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'}

129
docs/calculation.rst Normal file
View file

@ -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:

718
docs/cmdline_parser.rst Normal file
View file

@ -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
.. =================================
..
.. Lets 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:
..
.. - --<boolean_name>: it activates (set to True) the option
.. - --no-<boolean_name>: 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-<leader> 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 "Proxys 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 --<path> 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
..

149
docs/conf.py Normal file
View file

@ -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 <hello>` 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')

View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

138
docs/config.rst Normal file
View file

@ -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
<tiramisu.api.Config object at 0x7f3ee6204278>
:download:`download the getting started code <src/getting_started.py>`
Go futher with `Option` and `Config`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. toctree::
:maxdepth: 2
property
validator
calculation

View file

@ -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;
}

4
docs/custom/theme.conf Normal file
View file

@ -0,0 +1,4 @@
[theme]
inherit = bizstyle
stylesheet = custom.css

View file

@ -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

View file

@ -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, 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 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. 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 Tiramisu is an options handler and an options controller, which aims at
producing flexible and fast options access. The main advantages are its access 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 validations towards the whole options. Furthermore, options can be reached and
changed according to the access rules from nearly everywhere. 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: And then type:
```bash .. code-block:: bash
$ pip install tiramisu
```
### 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`. 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. We suggest using `git` if one wants to access to the current developments.
```bash .. code-block:: bash
$ git clone https://framagit.org/tiramisu/tiramisu.git
``` 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 This will get you a fresh checkout of the code repository in a local directory
named "tiramisu". named ``tiramisu``.

84
docs/glossary.rst Normal file
View file

@ -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`

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View file

@ -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, The tasting of `Tiramisu` --- `user documentation`
- it is also an [options controller tool](http://en.wikipedia.org/wiki/Configuration_management#Overview) ===================================================
It's a pretty small, local (that is, straight on the operating system) options handler and controller. .. 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:: .. toctree::
:maxdepth: 2 :maxdepth: 2
gettingstarted
config
browse
api_value
api_property api_property
storage
application application
quiz quiz
glossary glossary
External project: .. External project:
..
.. toctree:: .. .. toctree::
:maxdepth: 2 .. :maxdepth: 2
..
cmdline_parser .. cmdline_parser
.. FIXME ca veut rien dire : "AssertionError: type <class 'tiramisu.autolib.Calculation'> invalide pour des propriétés pour protocols, doit être un frozenset" .. FIXME ca veut rien dire : "AssertionError: type <class 'tiramisu.autolib.Calculation'> invalide pour des propriétés pour protocols, doit être un frozenset"

48
docs/leadership.rst Normal file
View file

@ -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])

BIN
docs/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

136
docs/option.rst Normal file
View file

@ -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)

View file

@ -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])

330
docs/options.rst Normal file
View file

@ -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

125
docs/own_option.rst Normal file
View file

@ -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')
<VowelOption object at 0x7feb2779c050>
>>> 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)

126
docs/property.rst Normal file
View file

@ -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'])

158
docs/quiz.rst Normal file
View file

@ -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 <src/quiz.py>`
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 !

81
docs/requirements.txt Normal file
View file

@ -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

18
docs/src/Makefile Normal file
View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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())

31
docs/src/api_value.py Normal file
View file

@ -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()

View file

@ -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()

View file

@ -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()

View file

@ -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()

232
docs/src/application.py Normal file
View file

@ -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()

73
docs/src/calculation.py Normal file
View file

@ -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()))

6
docs/src/find.py Normal file
View file

@ -0,0 +1,6 @@
"this is only to make sure that the tiramisu library loads properly"
cfg = Config(rootod)

View file

@ -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)

11
docs/src/own_option.py Normal file
View file

@ -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]*$")

43
docs/src/own_option2.py Normal file
View file

@ -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)

20
docs/src/property.py Normal file
View file

@ -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()

51
docs/src/proxy.py Normal file
View file

@ -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())

View file

@ -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())

132
docs/src/quiz.py Normal file
View file

@ -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)

133
docs/src/validator.py Normal file
View file

@ -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}"')

View file

@ -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)

View file

@ -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}"')

BIN
docs/storage.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

265
docs/storage.svg Normal file
View file

@ -0,0 +1,265 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="400"
height="200"
id="svg2"
version="1.1"
inkscape:version="0.48.4 r9939"
sodipodi:docname="test.svg"
inkscape:export-filename="/home/gnunux/git/tiramisu/doc/storage.png"
inkscape:export-xdpi="135"
inkscape:export-ydpi="135">
<defs
id="defs4">
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 526.18109 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="744.09448 : 526.18109 : 1"
inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
id="perspective3827" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="1"
inkscape:pageshadow="2"
inkscape:zoom="1"
inkscape:cx="106.95445"
inkscape:cy="208.15932"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1600"
inkscape:window-height="841"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Calque 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-852.36218)">
<path
style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 235.5,78.588237 306,109"
id="path4403"
inkscape:connector-type="polyline"
inkscape:connector-curvature="0"
inkscape:connection-start="#g4211"
inkscape:connection-start-point="d4"
transform="translate(0,852.36218)" />
<path
style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 235.5,131.08416 305,107"
id="path4405"
inkscape:connector-type="polyline"
inkscape:connector-curvature="0"
inkscape:connection-start="#g4216"
inkscape:connection-start-point="d4"
transform="translate(0,852.36218)" />
<g
id="g4206"
transform="translate(-17,590)">
<text
sodipodi:linespacing="686.00001%"
id="text2985"
y="368.36218"
x="98"
style="font-size:10px;font-style:normal;font-weight:normal;line-height:686.00001335%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
xml:space="preserve"><tspan
y="368.36218"
x="98"
id="tspan2987"
sodipodi:role="line">Config</tspan></text>
<rect
y="351.36218"
x="81"
height="30"
width="63"
id="rect3757"
style="fill:none;stroke:#000000;stroke-linejoin:round;stroke-opacity:1" />
</g>
<g
id="g4211"
transform="translate(-17,590)">
<rect
y="312.36218"
x="189.5"
height="30"
width="63"
id="rect3757-2"
style="fill:none;stroke:#000000;stroke-linejoin:round;stroke-opacity:1" />
<text
sodipodi:linespacing="686.00001%"
id="text3777"
y="330.36218"
x="206"
style="font-size:10px;font-style:normal;font-weight:normal;line-height:686.00001335%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
xml:space="preserve"><tspan
y="330.36218"
x="206"
id="tspan3779"
sodipodi:role="line">Values</tspan></text>
</g>
<g
id="g4216"
transform="translate(-17,590)">
<rect
y="389.36218"
x="189.5"
height="30"
width="63"
id="rect3757-4"
style="fill:none;stroke:#000000;stroke-linejoin:round;stroke-opacity:1" />
<text
sodipodi:linespacing="686.00001%"
id="text3799"
y="407.36218"
x="200"
style="font-size:10px;font-style:normal;font-weight:normal;line-height:686.00001335%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
xml:space="preserve"><tspan
y="407.36218"
x="200"
id="tspan3801"
sodipodi:role="line">Settings</tspan></text>
</g>
<path
style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 127,967.39444 45.5,15.93548"
id="path4028"
inkscape:connector-type="polyline"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 127,945.0396 45.5,-16.35484"
id="path4030"
inkscape:connector-type="polyline"
inkscape:connector-curvature="0" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none"
id="rect4161"
width="55.5"
height="26"
x="277.5"
y="946.36218" />
<path
sodipodi:type="arc"
style="fill:none;stroke:#000000;stroke-width:1.96347165;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="path3843"
sodipodi:cx="401"
sodipodi:cy="334.86218"
sodipodi:rx="38"
sodipodi:ry="10.5"
d="m 439,334.86218 a 38,10.5 0 1 1 -76,0 38,10.5 0 1 1 76,0 z"
transform="matrix(0.71325325,0,0,0.57998971,18.66254,749.17042)" />
<path
transform="matrix(0.71325325,0,0,0.57998971,18.57337,775.05247)"
sodipodi:type="arc"
style="fill:none;stroke:#000000;stroke-width:1.96347165;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="path3843-3"
sodipodi:cx="401"
sodipodi:cy="334.86218"
sodipodi:rx="38"
sodipodi:ry="10.5"
d="m 439,334.86218 a 38,10.5 0 1 1 -76,0 38,10.5 0 1 1 76,0 z" />
<path
transform="matrix(0.71325325,0,0,0.57998971,18.52879,762.07519)"
sodipodi:type="arc"
style="fill:none;stroke:#000000;stroke-width:1.96347165;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="path3843-3-0"
sodipodi:cx="401"
sodipodi:cy="334.86218"
sodipodi:rx="38"
sodipodi:ry="10.5"
d="m 439,334.86218 a 38,10.5 0 1 1 -76,0 38,10.5 0 1 1 76,0 z" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none"
id="rect3883"
width="62.989182"
height="6.7061315"
x="274.72043"
y="949.91193" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none"
id="rect3883-3"
width="58.087975"
height="6.4161367"
x="277.34818"
y="962.78046" />
<path
style="fill:none;stroke:#000000;stroke-width:1.26286423;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;display:inline"
d="m 277.52869,943.35095 -0.0442,26.02673"
id="path3917"
inkscape:connector-type="polyline"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#000000;stroke-width:1.26286423;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;display:inline"
d="m 331.64698,969.26909 0.13377,-26.17203"
id="path3921"
inkscape:connector-type="polyline"
inkscape:connector-curvature="0" />
<text
xml:space="preserve"
style="font-size:10px;font-style:normal;font-weight:normal;line-height:686.00001335%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
x="286.33643"
y="958.32324"
id="text3821"
sodipodi:linespacing="686.00001%"><tspan
sodipodi:role="line"
id="tspan3823"
x="286.33643"
y="958.32324">Storage</tspan></text>
<g
id="g4201"
transform="translate(-17,590)">
<rect
y="293.42468"
x="81"
height="30"
width="63"
id="rect3757-5"
style="fill:none;stroke:#000000;stroke-linejoin:round;stroke-opacity:1" />
<text
sodipodi:linespacing="100%"
id="text4190"
y="309.42468"
x="110.27588"
style="font-size:10px;font-style:normal;font-weight:normal;text-align:center;line-height:100%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
xml:space="preserve"><tspan
id="tspan4194"
y="309.42468"
x="110.27588"
sodipodi:role="line">Option</tspan></text>
</g>
<path
style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 95.5,913.42468 0,27.9375"
id="path4199"
inkscape:connector-type="polyline"
inkscape:connector-curvature="0" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.4 KiB

13
docs/symlinkoption.rst Normal file
View file

@ -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)

271
docs/validator.rst Normal file
View file

@ -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: