baldaquin.config — Configuration#

The module provides facilities to create, modify, read and write configuration objects. The basic ideas behind the mechanism implemented here is that:

  • configurations are split into sections; more specifically, top-level configuration objects are instances of the Configuration class, and each section is an instance of a class inheriting from the abstract base class ConfigurationSectionBase.

  • the configuration section contains a full set of default values for the configuration parameters, so that any instance of a configuration object is guaranteed to be valid at creation time—and, in addition, to remain valid as the parameter values are updated through the lifetime of the object;

  • type consistency is automatically enforced whenever a parameter is set or updated;

  • a minimal set of optional constraints can be enforced on any of the parameters;

  • a configuration object can be serialized/deserialized in JSON format so that it can be written to file, and the parameter values can be updated from file.

In the remaining of this section we shall see how to declare, instantiate and interact with concrete configuration objects.

Declaring configuration sections#

The module comes with a number of pre-defined configuration sections that are generally useful. Basically, all you have to do is to inherit from the abstract base class ConfigurationSectionBase and override the TITLE and _PARAMETER_SPECS top-level class members.

class MulticastConfigurationSection(ConfigurationSectionBase):

    """Configuration section for the packet multicasting.
    """

    TITLE = 'Multicast'
    _PARAMETER_SPECS = (
        ('enabled', bool, False, 'Enable multicast'),
        ('ip_address', str, '127.0.0.1', 'IP address'),
        ('port', int, 20004, 'Port', dict(min=1024, max=65535))
    )
class BufferingConfigurationSection(ConfigurationSectionBase):

    """Configuration section for the packet buffering.
    """

    TITLE = 'Buffering'
    _PARAMETER_SPECS = (
        ('flush_size', int, 100, 'Flush size', dict(min=1)),
        ('flush_timeout', float, 10., 'Flush timeout', 's', '.3f', dict(min=1.))
    )
class LoggingConfigurationSection(ConfigurationSectionBase):

    """Configuration section for the logging.
    """

    TITLE = 'Logging'
    _LOGGING_LEVELS = ('TRACE', 'DEBUG', 'INFO', 'SUCCESS', 'WARNING', 'ERROR', 'CRITICAL')
    _PARAMETER_SPECS = (
        ('terminal_level', str, 'DEBUG', 'Terminal logging level', dict(choices=_LOGGING_LEVELS)),
        ('file_level', str, 'DEBUG', 'File logging level', dict(choices=_LOGGING_LEVELS))
    )

The TITLE thing is pretty much self-explaining; PARAMETER_SPECS is supposed to be an iterable of tuples matching the ConfigurationParameter constructor, namely:

  1. the parameter name (str);

  2. the parameter type (type);

  3. the default value—note its type should match that indicated by the previous field;

  4. a string expressing the intent of the parameter;

  5. an optional string indicating the physical units of the parameter value (optional);

  6. an optional format string for the preferred text rendering of the value (optional);

  7. an optional dictionary encapsulating the constraints on the possible parameter values (optional).

The rules for parsing the specifications are simple: if the last element of the tuple is a dictionary we take it as containing all the keyword arguments for the corresponding parameters; all the other elements are taken to be positional arguments, in the order specified above. (Note only the first four elements are mandatory.)

The supported constraints are listed in the _VALID_CONSTRAINTS class variable of the ConfigurationParameter class, and they read:

    _VALID_CONSTRAINTS = {
        int: ('choices', 'step', 'min', 'max'),
        float: ('min', 'max'),
        str: ('choices',)
    }

(And if you look this closely enough you will recognize that the constraints are designed so that the map naturally to the GUI widgets that might be used to control the configuration, e.g., spin boxes for integers, and combo boxes for strings to pulled out of a pre-defined list.) Constraints are generally checked and runtime, and a RuntimeError is raised if any problem occurs.

Declaring configuration objects#

You can simply take it from here, and declare fully-fledged configuration objects by just instantiating the Configuration class, passing the configuration sections you want to include as arguments.

That said, given how baldaquin is structured, your configuration will likely contain a number of sections that are common to all the applications (e.g., logging, buffering, multicasting) and one or more section that is specific to the particular user application at hand. This common use case is handled by the UserApplicationConfiguration class, which is again a base class for usable application configuration objects.

By default it comes with no parameters in the user application section, but you can change that by overriding the _PARAMETER_SPECS class variable, just like you did for the configuration sections.

Once you have a concrete class defined, you can instantiate an object, which will come up set up and ready to use, with all the default parameter values.

>>> config = UserApplicationConfiguration()
>>> print(config)
----------Logging-------------
terminal_level...... INFO
file_enabled........ True
file_level.......... INFO
----------Buffering-----------
max_size............ 1000000
flush_size.......... 100
flush_timeout....... 10.000 s
----------Multicast-----------
enabled............. False
ip_address.......... 127.0.0.1
port................ 20004
----------User Application----

Programmatically, you can retrieve the value of a specific parameter through the value() class method, and update the value with set_value().

The save() method allows to dump the (JSON-encoded) content of the configuration into file looking like

{
    "Logging": {
        "terminal_level": "INFO",
        "file_enabled": true,
        "file_level": "INFO"
    },
    "Buffering": {
        "max_size": 1000000,
        "flush_size": 100,
        "flush_timeout": 10.0
    },
    "Multicast": {
        "enabled": false,
        "ip_address": "127.0.0.1",
        "port": 20004
    },
    "User Application": {}
}

and this is the basic mechanism through which applications will interact with configuration objects, with update_from_file() allowing to update an existing configuration from a JSON file with the proper format.

Note

Keep in mind that configurations are never read from file—they come to life with all the parameters set to their default values, and then they can be updated from a JSON file.

When you think about, this makes extending and/or modifying existing configurations much easier as, once the concrete class is changed, all existing configuration files are automatically updated transparently, and in case one edits a file by hand, any mistake will be promptly signaled (and corrected) without compromising the validity of the configuration object.

Module documentation#

Configuration facilities.

class baldaquin.config.ConfigurationParameter(name: str, type_: type, value: Any, intent: str, units: str = None, fmt: None = None, **constraints)[source]#

Class representing a configuration parameter.

This is a simple attempt at putting in place a generic configuration mechanism where we have some control on the values we are passing along.

A configuration parameter is fully specified by its name, type and value, and When setting the latter, we make sure that the its type matches. Additional, we can specify simple conditions on the parameters that are then enforced at runtime.

Parameters:
  • name (str) – The parameter name.

  • type (type) – The parameter type.

  • value (anything) – The parameter value.

  • intent (str) – The intent of the parameter, acting as a comment in the corresponding configuration file.

  • units (str, optional) – The units for the configuration parameter.

  • fmt (str, optional) – An optional format string for the preferred rendering the parameter value.

  • constraints (dict, optional) – A dictionary containing optional specifications on the parameter value.

_VALID_CONSTRAINTS = {<class 'float'>: ('min', 'max'), <class 'int'>: ('choices', 'step', 'min', 'max'), <class 'str'>: ('choices',)}#
_check_range(value: Any) None[source]#

Generic function to check that a given value is within a specified range.

_check_choices(value: Any) None[source]#

Generic function to check that a parameter value is within the allowed choices.

_check_step(value: int) None[source]#

Generic function to check the step size for an integer.

set_value(value: Any) None[source]#

Set the parameter value.

Note that this is where all the runtime checking is performed.

formatted_value() str[source]#

Return the formatted parameter value (as a string).

pretty_print() str[source]#

Return a pretty-printed string representation of the parameter.

class baldaquin.config.ConfigurationSectionBase[source]#

Base class for a single section of a configuration object. (Note this class is not meant to be instantiated, but rather subclassed, overriding the proper class variables as explained below.)

The basic idea, here, is that specific configuration classes simply override the TITLE and _PARAMETER_SPECS class members, the latter encoding the name, type and default values for all the configuration parameters, as well as optional help strings and constraints.

The class interface is fairly minimal, with support to set, and retrieve parameter values, and for string formatting.

TITLE = None#
_PARAMETER_SPECS = ()#
set_value(parameter_name, value) None[source]#

Update the value of a configuration parameter.

value(parameter_name) Any[source]#

Return the value for a given parameter.

formatted_value(parameter_name) str[source]#

Return the formatted value for a given parameter.

as_dict()[source]#

Return a view on the configuration in the form of a {name: value} dictionary representing the underlying configuration parameters.

This is used downstream to serialize the configuration and writing it to file.

class baldaquin.config.LoggingConfigurationSection[source]#

Configuration section for the logging.

TITLE = 'Logging'#
_LOGGING_LEVELS = ('TRACE', 'DEBUG', 'INFO', 'SUCCESS', 'WARNING', 'ERROR', 'CRITICAL')#
_PARAMETER_SPECS = (('terminal_level', <class 'str'>, 'DEBUG', 'Terminal logging level', {'choices': ('TRACE', 'DEBUG', 'INFO', 'SUCCESS', 'WARNING', 'ERROR', 'CRITICAL')}), ('file_level', <class 'str'>, 'DEBUG', 'File logging level', {'choices': ('TRACE', 'DEBUG', 'INFO', 'SUCCESS', 'WARNING', 'ERROR', 'CRITICAL')}))#
class baldaquin.config.BufferingConfigurationSection[source]#

Configuration section for the packet buffering.

TITLE = 'Buffering'#
_PARAMETER_SPECS = (('flush_size', <class 'int'>, 100, 'Flush size', {'min': 1}), ('flush_timeout', <class 'float'>, 10.0, 'Flush timeout', 's', '.3f', {'min': 1.0}))#
class baldaquin.config.MulticastConfigurationSection[source]#

Configuration section for the packet multicasting.

TITLE = 'Multicast'#
_PARAMETER_SPECS = (('enabled', <class 'bool'>, False, 'Enable multicast'), ('ip_address', <class 'str'>, '127.0.0.1', 'IP address'), ('port', <class 'int'>, 20004, 'Port', {'max': 65535, 'min': 1024}))#
class baldaquin.config.Configuration(*sections: ConfigurationSectionBase)[source]#

Class describing a configuration object, that is, a dictionary of instances of ConfigurationSectionBase subclasses.

Configuration objects provide file I/O through the JSON protocol. One important notion, here, is that configuration objects are always created in place with all the parameters set to their default values, and then updated from a configuration file. This ensures that the configuration is always valid, and provides an effective mechanism to be robust against updates of the configuration structure.

add_section(section: ConfigurationSectionBase) None[source]#

Add a section to the configuration.

update_from_file(file_path: str) None[source]#

Update the configuration dictionary from a JSON file.

Note we try and catch here all the possible exceptions while updating a file, and if anything happens during the update we create a timestamped copy of the original file so that the thing can be debugged at later time. The contract is that the update always proceeds to the end, and all the fields that can be legitimately updated get indeed updated.

as_dict() dict[source]#

Return a view on the configuration in the form of a dictionary that can be used for serialization.

to_json(indent: int = 4) str[source]#

Encode the configuration into JSON to be written to file.

save(file_path: str) None[source]#

Dump the configuration dictionary to a JSON file.

class baldaquin.config.UserApplicationConfiguration[source]#

Base class for a generic user application configuration.

_USER_APPLICATION_SECTION_TITLE = 'User Application'#
_PARAMETER_SPECS = ()#
overwrite_section(section: ConfigurationSectionBase) None[source]#

Overwrite a section in the configuration.

logging_section()[source]#

Return the logging section of the configuration.

buffering_section()[source]#

Return the buffering section of the configuration.

multicast_section()[source]#

Return the multicast section of the configuration.

application_section()[source]#

Return the user application section of the configuration.