Configuration schema validation

As noted in index, your configuration is mostly supposed to be a dict. To validate your schema, you should instantiate a Descriptor. Descriptor reflects how your config is nested.

class satella.configuration.schema.Boolean

This value must be a boolean, or be converted to one

class satella.configuration.schema.Float

This value must be a float, or be converted to one

class satella.configuration.schema.Integer

This value must be an integer, or be converted to one

class satella.configuration.schema.String

This value must be a string, or be converted to one

class satella.configuration.schema.File

This value must be a valid path to a file. The value in your schema will be an instance of FileObject

class satella.configuration.schema.FileObject(path: str)

What you get for values in schema of File.

This object is comparable and hashable, and is equal to the string of it’s path

get_value(encoding: str | None = None) str | bytes

Read in the entire file into memory

Parameters:

encoding – optional encoding to apply. If None given, bytes will be returned

Returns:

file contents

open(mode: str)

Open the file in specified mode

Parameters:

mode – mode to open the file in

Returns:

file handle

class satella.configuration.schema.FileContents(encoding: str | None = None, strip_afterwards: bool = False)

This value must be a valid path to a file. The value in your schema will be the contents of this file, applied with encoding (if given). By default, bytes will be read in

class satella.configuration.schema.Directory

This value must be a valid path to a file. The value in your schema will be an instance of FileObject

class satella.configuration.schema.DirectoryObject(path: str)

What you get for values in schema of Directory.

This object is comparable and hashable, and is equal to the string of it’s path

get_files() Iterable[str]

Return a list of files inside this directory :return:

class satella.configuration.schema.basic.FileObject(path: str)

What you get for values in schema of File.

This object is comparable and hashable, and is equal to the string of it’s path

class satella.configuration.schema.IPv4

This must be a valid IPv4 address (no hostnames allowed)

class satella.configuration.schema.List(type_descriptor: Descriptor | None = None)

This must be a list, made of entries of a descriptor (this is optional)

class satella.configuration.schema.Dict(keys: ~typing.List[DictDescriptorKey], unknown_key_mapper: ~typing.Callable[[str, int | float | str | dict | list | bool | None], ~typing.Any] = <function Dict.<lambda>>)

This entry must be a dict, having at least specified keys.

Use like:

>>> Dict([
>>>     create_key(String(), 'key_s'),
>>>     create_key(Integer(), 'key_i'),
>>>     create_key(Float(), 'key_f'),
>>>     create_key(String(), 'key_not_present', optional=True,
>>>                default='hello world'),
>>>     create_key(IPv4(), 'ip_addr')
>>>])
class satella.configuration.schema.Caster(to_cast: Callable[[Any], Any])

A value must be ran through a function.

Use like:

>>> class Environment(enum.IntEnum):
>>>     PRODUCTION = 0
>>> assert Caster(Environment)(0) == Environment.PRODUCTION

Then there is a descriptor that makes it possible for a value to have one of two types:

class satella.configuration.schema.Union(*descriptors: List[Descriptor])

The type of one of the child descriptors. If posed as such:

Union(List(), Dict())

then value can be either a list or a dict

You can use the following to declare your own descriptors:

class satella.configuration.schema.Descriptor

Base class for a descriptor

class satella.configuration.schema.Regexp

Base class for declaring regexp-based descriptors. Overload it’s attribute REGEXP. Use as following:

>>> class IPv6(Regexp):
>>>     REGEXP = '(([0-9a-f]{1,4}:)' ...

Just remember to decorate them with

satella.configuration.schema.register_custom_descriptor(name: str, is_plain: bool = True)

A decorator used for registering custom descriptors in order to be loadable via descriptor_from_dict

Use like:

>>> @register_custom_descriptor('ipv6')
>>> class IPv6(Regexp):
>>>     REGEXP = '(([0-9a-f]{1,4}:)' ...
Parameters:
  • name – under which it is supposed to be invokable

  • is_plain – is this a nested structure?

If you want them loadable by the JSON-schema loader.

You use the descriptors by calling them on respective values, eg.

>>> List(Integer())(['1', '2', 3.0])
[1, 2, 3]

JSON schema

The JSON schema is pretty straightforward. Assuming the top-level is a dict, it contains keys. A key name is the name of the corresponding key, and value can have two types. Either it is a string, which is a short-hand for a descriptor, or a dict containing following values:

{
    "type": "string_type",
    "optional": True/False,
    "default": "default_value" - providing this implies optional=True
}

Note that providing a short-hand, string type is impossible for descriptors that take required arguments.

Available string types are:

You can use file contents as follows:

{
    "contents": {
        "type": "file_contents",
        "encoding": "utf-8
    }
}

Or just

{
    "contents": "file_contents"
}

But in this case, bytes will be read in.

Lists you define as following

{
    "type": "list",
    "of": {
        ".. descriptor type that this list has to have .."
    }
}

Unions you define the following

{
    "type": "union",
    "of": [
        ".. descriptor type 1 ..",
        ".. descriptor type 2 .."
        ]
}

Dicts are more simple. Each key contains the key that should be present in the dict, and value is it’s descriptor - again, either in a short form (if applicable) or a long one (dict with type key).

You load it using the following function:

satella.configuration.schema.descriptor_from_dict(dct: dict) Descriptor

Giving a Python dictionary-defined schema of the configuration, return a Descriptor-based one

Parameters:

dct – something like

{

“a”: “int”, “b”: “str”, “c”: {

“type”: “int” “optional”: True, “default”: 5

}, “d”: {

“a”: “int”, “b”: “str”

}

}

although you can pass “int”, “float” and “str” without enclosing quotes, that will work too

Returns:

a Descriptor-based schema

Casters you define as

{
    "type": "caster"
    "cast_to": "name of a built-in or a fully qualified class ID"
}

If cast_to is not a builtin, it specifies a full path to a class, which will be loaded using satella.imports.import_class().

Additionally, an extra argument can be specified:

{
    "type": "caster",
    "cast_to": "name of a built-in or a FQ class ID",
    "expr": "y(int(x))"
}

In which case cast_to will be displayed as a y in expression, which will be eval()ed, and this value will be output. The input value will be called x.

You can also provide a commentary for your entries:

{
    "contents": {
        "type": "file_contents",
        "encoding": "utf-8,
        "description": "Encryption key (private key)",
        "strip_afterwards": True
    },
    "max_workers": {
        "type": "int",
        "description": "Maximum parallel instances of service"
    }
}

strip_afterwards (default is False) strips the content of loaded file of trailing and leading whitespace.