Skip to content

Package

formaldict

formaldict.Errors

Errors()

Bases: Mapping

Collects errors found when validating a Schema

Source code in formaldict/core.py
def __init__(self):
    self._errors = collections.defaultdict(list)

formaldict.FormalDict

FormalDict(*, schema, parsed, data, errors)

Bases: Mapping

A formal dictionary of data associated with a Schema.

Formal dictionaries can be created two ways::

1. Schema(...).parse(data)
2. Schema(...).prompt()

After creation, attributes from the Schema can be accessed as attributes on the dictionary.

Source code in formaldict/core.py
def __init__(self, *, schema, parsed, data, errors):
    assert isinstance(parsed, dict)
    assert isinstance(schema, Schema)
    assert isinstance(errors, Errors)

    self._schema = schema
    self._data = data
    self._parsed = parsed
    self._errors = errors

data property

data

The raw data used when constructing the formal dictionary

errors property

errors

Returns an Errors class if any errors happened during parsing

is_valid property

is_valid

True if the dictionary is valid and all attributes are present from the schema

parsed property

parsed

The parsed "formal" data.

Accessing the dictionary (i.e. data[key]) returns parsed data

formaldict.Schema

Schema(schema)

Bases: Sequence

The Schema object provides a definition of structured data and associated methods to parse and validate structured data.

Below is an example of a schema that represents a "name" and a "description"::

schema = Schema([{
    'label': 'description'
}, {
    'label': 'name'
}])

Each schema entry requires the "label", a unique ID for the attribute being parsed, and a "type" that specifies the type of data being parsed.

After declaring the schema, one can parse dictionaries with Schema.parse(). For example::

data = schema.parse({'description': 'My Description', 'name': 'John'})

In the above, data is a FormalDict object, which can be accessed as a dictionary. If data.is_valid is True, it means that the data passed schema validation and was successfully parsed. data.errors will contain all of the schema errors that were found.

The Schema object directly integrates with python-prompt-toolkit <https://github.com/prompt-toolkit/python-prompt-toolkit>_, allowing one to prompt for information based on the schema.

For example::

data = schema.prompt()

The above will result in the user being prompted for all attributes in the schema in the order in which they were defined. Any data that passes the prompt will also pass the schema validation, ensuring that valid data is always returned from prompting.

The Schema object provides the ability to configure prompt help text, multi-line input, and parsing other types of data. See more examples below, which provide more information on schema attributes.

Examples:

The schema below obtains a formal dictionary with information about a
person - a name, address, marital status, and date of birth::

    schema = Schema([{
        # The label is required and is the resulting key in the formal
        # dictionary
        'label': 'name',

        # The name is a human-readable description of the label. It is used
        # when prompting the user for the information. It defaults to
        # a title-ized version of the label.
        'name': 'Full Name',

        # The help is displayed when prompting the user.
        'help': 'Your full name (first, middle, and last).'

        # The type defaults to string. Currently formaldict supports
        # string and datetime types.
        'type': 'string'
    }, {
        'label': 'address',
        'help': 'Enter your street, city, state, and zipcode',

        # The multiline attribute turns on multi-line mode when
        # prompting the user.
        'multiline': True
    }, {
        'label': 'marital_status',
        'help': 'Your current marital status.',

        # The user is only allowed to enter the provided choices.
        'choices': [
            'single', 'separated', 'widowed', 'divorced', 'married'
        ],
    }, {
        'label': 'dob',
        'name': 'Date of Birth',
        'help': 'Your birthday (YYYY/MM/DD)',

        # datetime types are parsed either as integers (unix timestamps)
        # or by any format accepted by dateutil
        # (https://dateutil.readthedocs.io/en/stable/)
        'type': 'datetime',

        # Everything is required by default.
        'required': False,
    }])

.. note::

    When declaring a schema, the order of entries will be the order
    in which the user is prompted for information.

Schemas allow conditional collection of information. For example,
say that you want to create a schema where the user enters Jira
ticket numbers and extended descriptions for all non-trivial types of
changes to a project::

    schema = Schema([
        'label': 'type',
        'help': 'The type of change being committed',
        'choices': ['bug', 'feature', 'trivial'],
    }, {
        'label': 'description',
        'multiline': True,
        'help': 'An extended description of the change.',

        # Conditions are kmatch patterns
        # (https://github.com/ambitioninc/kmatch)
        # that must validate against the labels in previous steps.
        # This kmatch pattern asserts that we only collect
        # the description if the "type" entered by the user
        # is not the "trivial" choice.
        'condition': ['!=', 'type', 'trivial']
    }, {
        'label': 'jira',
        'help': 'The Jira ticket number',
        'condition': ['!=', 'type', 'trivial'],

        # Use a regex for validating input.
        'matches': 'PROJ-\d+'
    }])

In the above, the user would not be prompted for the "description" and
"jira" steps if they entered "trivial" as the type of change.
Similarly, one must enter a proper Jira ticket in order to pass
the parsing of information. Here are some examples of parsing payloads
that fail the schema::

    # The jira ticket is not required for trivial changes. Remember,
    # the "strict" flag verifies that all keys in the payload
    # match keys that are required (or conditionally required) by
    # the schema.
    schema.parse({
        'type': 'trivial',
        'jira': 'PROJ-111'
    }, strict=True)

    # The jira ticket is required, but it does not match the pattern.
    schema.parse({
        'type': 'bug',
        'description': 'Description of bug'
        'jira': 'invalid-ticket'
    })

Parameters:

Name Type Description Default
schema dict

Schema rules

required
Source code in formaldict/core.py
def __init__(self, schema):
    """
    Initialize the schema object

    Args:
        schema (dict): Schema rules
    """
    assert isinstance(schema, (list, tuple))

    # Keep a copy of the original raw schema given by the user
    self._raw_schema = copy.deepcopy(schema)

    # Instantiate the cleaned schema (i.e. self._schema)
    self._clean()

parse

parse(data, strict=False) -> FormalDict

Parse data based on the schema.

Parameters:

Name Type Description Default
strict boolean, default=False

If True, add a ValidationError to self.errors if any keys in the entry are not present in the schema.

False

Returns:

Type Description
FormalDict

The parsed data

Todo

Allow passing in default values to override any defaults in the schema.

Source code in formaldict/core.py
def parse(self, data, strict=False) -> FormalDict:
    """
    Parse data based on the schema.

    Args:
        strict (boolean, default=False): If True, add a
            ValidationError to self.errors if any keys in the entry
            are not present in the schema.

    Returns:
        The parsed data

    Todo:
        Allow passing in default values to override any defaults in the
        schema.
    """
    parsed = {}
    errors = Errors()
    condition_failed_labels = set()
    label = ""
    for entry_schema in self:
        try:
            label = entry_schema["label"]
            if not self.passes_condition(entry_schema, parsed):
                condition_failed_labels.add(label)
                continue

            parsed[label] = self._parse_entry(label, data.get(label))
        except exceptions.ValidationError as exc:
            errors.add(exc, label=label)

    non_extant_labels = set(data.keys()) - set(self._entry_schemas.keys())
    if strict and non_extant_labels:
        err_msg = 'Labels "' + ", ".join(non_extant_labels) + '" not present in schema.'
        errors.add(exceptions.ValidationError(err_msg))

    condition_failed_labels = set(data.keys()) & condition_failed_labels
    if strict and condition_failed_labels:
        err_msg = (
            'Labels "' + ", ".join(condition_failed_labels) + '" failed conditions in schema.'
        )
        errors.add(exceptions.ValidationError(err_msg))

    return FormalDict(schema=self, parsed=parsed, data=data, errors=errors)

prompt

prompt(defaults=None) -> FormalDict

Prompt for input of all entries in the schema

Parameters:

Name Type Description Default
defaults dict, default=None

Default values for the schema that should be used in place of any other declared defaults

None

Returns:

Type Description
FormalDict

The parsed information, which also validates against the

FormalDict

Schema.

Source code in formaldict/core.py
def prompt(self, defaults=None) -> FormalDict:
    """
    Prompt for input of all entries in the schema

    Args:
        defaults (dict, default=None): Default values for the schema
            that should be used in place of any other declared defaults

    Returns:
        The parsed information, which also validates against the
        `Schema`.
    """
    defaults = defaults or {}
    data = {}

    for entry_schema in self:
        if not self.passes_condition(entry_schema, data):
            continue

        label = entry_schema["label"]
        choices = entry_schema.get("choices")
        help_text = self._get_help_text(entry_schema["label"])
        prompt_text = self._get_prompt_text(entry_schema["label"])
        validator = _ValueValidator(schema=self, label=label)

        value = prompt_toolkit.prompt(
            prompt_text,
            bottom_toolbar=HTML(help_text) if help_text else None,
            completer=WordCompleter(choices) if choices else None,
            default=defaults.get(label, entry_schema["default"]),
            validator=validator,
            validate_while_typing=False,
            prompt_continuation="> ",
            multiline=entry_schema["multiline"],
        )
        if value.strip():
            data[label] = value

    return self.parse(data)

formaldict.exceptions

formaldict.exceptions.Error

Bases: Exception

The base error for all formaldict errors

formaldict.exceptions.SchemaError

Bases: Error

When an issue is found in the user-supplied schema

formaldict.exceptions.ValidationError

Bases: Error

When a schema validation error happens