Source code for cion.schema

"""Objects for defining schema to validate data"""
from collections import defaultdict
from typing import Any, Callable, Optional

from cion.exceptions import ValidationError, ValidatorError
from cion.options import ExtraFields, Options

__all__ = (
    "Field",
    "Schema",
)

Validator = Callable[[Any], Any]

RESERVED_ERROR_KEY = "__schema__"


[docs]class Field: """A field in a Schema""" filters: list[Validator] default: Optional[Any] = None nullable: bool required: bool = False
[docs] def __init__( self, *, filters: Optional[list[Validator]] = None, default: Any = None, nullable: bool = False, required: bool = False, ) -> None: """Create a field for use in a Schema data is assumed to be the data passed to :meth:`Schema.validate` Args: filters: A list of filters that get passed against the value default: The default value for the field, if it is not present in the data nullable: Whether the value can be None When this is ``True`` and the value is None, no filters are called required: Whether or not the field is required to be in the data """ if required is True and default is not None: raise ValueError("Cannot have a default if value is not required") self.filters = filters or [] self.default = default self.nullable = nullable self.required = required
[docs]class Schema: """Schema to validate data""" fields: dict[str, Field] options: Options
[docs] def __init__(self, fields: dict[str, Field], options: Optional[Options] = None) -> None: """Create schema instance Initializes a schema instance that can be used to validate data Args: fields: A dictionary of fields, with the keys being names and the values being an instance of :class:`cion.Field` options: Optional options to add extra functionality to the schema Notes: ``__schema__`` is not allowed to be a field name, it is reserved for internal usage Raises: ValueError: If ``__schema__`` is included as a field name """ if RESERVED_ERROR_KEY in fields: raise ValueError(f"{RESERVED_ERROR_KEY} cannot be a field name, it is reserved for internal purposes") self.fields = fields self.options = options or Options()
[docs] def validate(self, data: dict[Any, Any]) -> dict[Any, Any]: """Validate a dict according to the defined schema Goes through every item in the data and applies constraints to it Note: It is important to ensure that ``data`` is an iterable that can have data removed. It is highly reccomended to convert it to a dict before calling this method. Args: data: The data to be validated Returns The validated and transformed data depending on the specified validators Raises: ValidationError: When ``self.stop_on_error`` is true and a field in the data does not validate properly ValidationError: When ``self.stop_on_error`` is false, this will contain all the errors, if any """ errors = defaultdict(list) filtered: dict[str, Any] = {} def raise_error(field_name: str, error_message: str, *, delete_field: bool = True): if delete_field is True: del data[field_name] if self.options.stop_on_error is True: raise ValidationError({field_name: [error_message]}, {}) errors[name].append(error_message) for name, options in self.fields.items(): # We can't use dict.get since the value may be allowed to be None try: value = data[name] except KeyError: if options.required is True and options.default is None: raise_error(name, "This field is required", delete_field=False) continue if options.default is not None: value = options.default else: continue if value is None: if options.nullable is not True: raise_error(name, "This field is not allowed to be None") continue for filter_ in options.filters: if value is not None: try: value = filter_(value) except Exception as error: if isinstance(error, ValidatorError): raise_error(name, error.message) continue filtered[name] = value # We don't need to account for ExtraFields.IGNORE # since IGNORE means don't do anything # Since the validated data is deleted from the original data # anything left over is extra if self.options.extra is ExtraFields.COMBINE: filtered = filtered | data if self.options.extra is ExtraFields.ERROR: raise_error(RESERVED_ERROR_KEY, f"Found extra data: {', '.join(data.keys())}", delete_field=False) if bool(errors): # Checks if there are any keys raise ValidationError(errors, filtered) return filtered