diff --git a/README.md b/README.md index e69de29..e6efbd1 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,86 @@ +# Config-Spec + +A Python library for specifying config requirements. Enables configuration to be validated before being loaded and used. + +At it's simplest, create a spec and validate values against it: +```python +>>> from configspec import * +>>> confspec=IntSpec(maxval=12, helptext="An int below 12") +>>> confspec.validate(3) +``` +An invalid value will throw an `InvalidConfigError` exception: +```python +>>> confspec.validate(42) + +configspec.specification.InvalidConfigError: Config value must be <= 12 +``` + +Config-Spec extends these basics to also include recursive nesting, optional values and defaults, portable/serialised specifications, and configuration overlays. + +## Why yet another config library? + +The original need for Config-Spec was to allow portable plugins to define what sort of config they needed, but have another core program actually load, manage and verify the config before handing it off to them. The goal was to have something simple and easy to use, without getting bogged down in complexities and boilerplate. + +While it includes direct support for TOML, Config-Spec is happy to use any generic Python data structure. The specification and validation features can also be put to good use by themselves - the loading, layering and management functionality is split apart and completely optional. + +# Installation +Install Config-Spec with Pip directly from this repo: + +```bash +pip install git+https://git.distreon.net/novirium/config-spec.git +``` + +For development setup, see the [the Development guide](./DEVELOPMENT.md) + +# Usage + +## Specifications and validations + +Specifications can be defined in 3 basic Python primary types (``BoolSpec``, ``IntSpec`` and ``StringSpec``), along with dictionaryies to organise them (``DictSpec``). The ``ConfigSpecification`` is just a special type of ``DictSpec`` used to indicate the root of the config. + +All of the specification types also have a list variant (``BoolListSpec``, ``IntListSpec``, ``StringListSpec``, ``DictListSpec``). + +The general idea is to define your config specification early on, then use it to validate whatever configuration is loaded before starting up your application - allowing it to bail out gracefully or fall back to another configuration. + +For example, here we create a root config specification dict `confspec` and add 3 keys to it of different types: +```python +from configspec import * +confspec = ConfigSpecification() +confspec.add_spec("my_num", IntSpec(maxval=12, helptext="An int below 12")) +confspec.add_spec("my_text_items", StringListSpec(helptext="A list of strings")) +confspec.add_spec("option_flag", BoolSpec(default=False, optional=True, helptext="An optional bool")) +``` +We can see ``add_spec()`` adding the new specification keys, giving them a name and a type, along with specific requirements to validate against. Note that the second spec added (``"my_text_items"``) is a single specification that will then validate against every item in the list given to it. + +The config spec defined above should then validate a properly structured config: +```python +loaded_conf = {"my_num":10, "my_text_items": ["string1","string2","string3"]} +confspec.validate(loaded_conf) +``` +If all is valid, nothing should really happen - when ``validate()`` is called an exception will be thrown if something doesn't fit properly, so any code after this point can safely assume that the ``loaded_conf`` dict contains the keys and values it expects it to. + +The above example also demonstrates the ``optional`` arg that can be given to a specification - if not supplied in the dict to be validated, it will be added, and given a the default value (yes, this _does_ mean calling ``validate()`` on some data may modify it). For instance, we can then run the following, showing that the ``"option_flag"`` now exists: +```python +>>> print(loaded_conf["option_flag"]) +False +``` + +## Portable specifications +One of the dependancies for Config-Spec is [python-preserve](https://git.distreon.net/novirium/python-preserve), and all Specification classes are ``preservable``. This means that config specifications can be easily preserved to simple python types, serialised (using whatever means you prefer) and restored elsewhere: + +```python +(using confspec from above example) + +>>> import preserve +>>> preserved_spec=preserve.preserve(confspec) + +>>> preserved_spec +{'default': None, 'optional': False, 'helptext': '', 'spec_dict': {'my_num': {'default': None, 'optional': False, 'helptext': 'An int below 12', 'minval': None, 'maxval': 12, '<_jam>': 'IntSpec'}, 'my_text': {'default': '', 'optional': False, 'helptext': 'A list of strings', 'minlength': None, 'maxlength': None, '<_jam>': 'StringListSpec'}, 'option_flag': {'default': False, 'optional': True, 'helptext': 'An optional bool', '<_jam>': 'BoolSpec'}}, '<_jam>': 'ConfigSpecification'} + +>>> preserve.restore(preserved_spec) + +``` + + +## Config Manager +Config-Spec includes a config manager to help load and organise different bundles and layers of configuration values, but this is still changing regularly. Use of this manager is completely optional, and is all self-contained in [manager.py](configspec/manager.py) \ No newline at end of file diff --git a/configspec/specification.py b/configspec/specification.py index 6866e2a..b00189a 100644 --- a/configspec/specification.py +++ b/configspec/specification.py @@ -1,38 +1,6 @@ """ -Configuration management module. Enables configuration to be validated against -requirement specifications before being loaded and used. - -Compatible with both raw config data structures and TOML files, config data must -start with a root dict containing named "config bundles". These are intended to -refer to different modular parts of the application needing configuration, and the -config data structure must contain at least one. - -Each config bundle itself needs to have a dict at the root, and so in practice a minimal -TOML config file would look like:: - - [myapp] - config_thingy_a = "foooooo!" - important_number = 8237 - -This would resolve to a config bundle named "myapp" that results in the dict:: - - {"config_thingy_a": "foooooo!", "important_number": 8237} - -Root items that are not dicts are not supported, for instance both the following -TOML files would fail:: - - [[myapp]] - important_number = 8237 - [[myapp]] - another_important_number = 2963 - -(root object in bundle is a list) - -:: - - root_thingy = 46 - -(root object in config is a single value) +A Python library for specifying config requirements. Enables configuration to be validated before +being loaded and used. """ from abc import ABC, abstractmethod