Package: dhampyr
Python simple validator for dictionary-like objectsOverviewThis library provides data validation functionalities designed for HTTP applications. Compared to other validation libraries, this library has following features.Validation schemes are declared like dataclass field specifier.Each validation scheme can be composed of simple functions including lambda expressions.Errors in validations are represented with informative objects, not with just error messages.InstallationThis library requires python 3.6 or higher.$ pip install dhampyr==1.0-a3TutorialDeclaration of validation schemeThe moduledhampyrexports a functionvwhich creates aValidator. This function is designed to be used likedataclassfield specifier.from dhampyr import *

class C:
    a: int = +v(..., lambda x: x < 5, lambda x: x > 2, default=0)vcan be used as the metadata inAnnotatedannotation. In this case, the value assigned to the attribute becomes a default value.from dhampyr import *
from typing import Annotated

class C:
    a: Annotated[int, +v(..., lambda x: x < 5, lambda x: x > 2)] = 0vhas additional keyword arguments. For example,aliasspecifies the corresponding key in the input explicitly, Arguments ofdataclassfield specifier such asdefaultordefault_factoryare also available. See API document to know all possible arguments.The validation scheme starts with the declaration ofvalidatable typelikeCand an input dict-like object. Each key-value pair in the input becomes an input value to an attribute whose name (or specified alias) matches to the key. For each attribute, three phases run in order.Checks whether the input value exists or not.Requirementsetting of theValidatordetermines the behavior according to the existence.Converts the input value byConverterwhich by default is the annotated type of the attribute. Conversion function can be set in the first argument ofv.Converted value is verified byVerifiers given in positional arguments inv. A function which takes a converted value and returnsboolis the most simple one representingVerifier.The scheme finally returns an instance of thevalidatable typewhere values passing those phases are assigned to the attributes.validate_dictis the function which runs the scheme.r = validate_dict(C, dict(a="3"))
d = r.get()

assert bool(r)
assert type(d) == C
assert d.a == 3validate_dictreturns aValidationResultobject which contains validated instance and errors. It also is available in boolean context to check if the scheme has succeeded or not.Composite validationNested validatable types are available without any special declaration.from dhampyr import *

class D:
    a: int = v()

class C:
    a: D = v()

r = validate_dict(C, dict(a=dict(a="3")))
d = r.get()

assert type(d) == C
assert type(d.a) == D
assert d.a.a == 3Each value in iterable input are converted and verified respectively. In this case,CoverterandVerifiers should be declared as alistwhich includes their specifier. Nested type declaration are available as well.class D:
    a: int = v()

class C:
    a: list[int] = v(..., [lambda x: x > 0])
    b: list[D] = v()

r = validate_dict(C, dict(a=[1, 2, 3], b=[dict(a=4), dict(a=5), dict(a=6)]))
d = r.get()

assert d.a == [1, 2, 3]
assert [b.a for b in d.b] == [4, 5, 6]Error handlingEvery kind of error in the validation scheme is repreesented withValidationFailureobject which can be accessed viafailuresattribute ofValidationResult. This attributes is always notNoneand tells where and what kind of error happened in an invocation ofvalidate_dict.EvaluateValidationResultasboolto know every validation scheme succeeded or not.lenreturns the number of erroneous keys (on the root validated instance, nested errors are not counted).inoperator is available to know whether errors happened at the specific key.Access with Square bracket ([]) returns the error at the key, orNoneif no error.Iteration yields all errors including nested ones in depth-first traversal order.from dhampyr import *

def lt3(x):
    return x < 3
def gt1(x):
    return x > 1

class C:
    a: int = v(default=0)
    b: int = v(..., lt3, default=0)
    c: int = v(..., lt3, gt1, default=0)

r = validate_dict(C, dict(a="a", b="3", c="1"))

assert bool(r) is False
assert len(r.failures) == 3
assert "a" in r.failures
assert r.failures["a"].name == "int"
assert dict([(str(k), f.name) for k, f in r.failures]) == {"a": "int", "b": "lt3", "c": "gt1"}EachValidationFailurehas anameattribute which corresponds to the name ofConverterorVerifiercausing the error. You can recognize the cause of each error by this attribute. Basically the name is set to__name__of the function used to declare them, but there are various ways to set the name explicitly as described below.Next table shows predefined names.namecausemalformedInput value was not dictionary-like.missingThe key did not exist in input dictionary.nullInput value wasNone.emptyInput value was determined to beempty.Composite validation makes errors hierarchical. You should usepathcomposed of string keys and numerical indexes. To get an error at the specific position, apply path components with square bracket in order. On the other hand, iteration overValidationFailureyields pairs of path and error in depth-first traversal order, where the path is represented withValidationPathobject. It provides intuitive textual representation likea.b[0].c[0].d.from dhampyr import *

class D:
    b: list[int] = v(default_factory=list)

class C:
    a: list[D] = v(default_factory=list)

r = validate_dict(C, dict(a=[dict(b="123"), dict(b="45a"), dict(b="789")]))

assert r.failures["a"][1]["b"][2].name == "int"
assert [(str(p), list(p)) for p, f in r.failures] == [("a[1].b[2]", ["a", 1, "b", 2])]As shown in the above example, developers can get complete information why and where the validation failed. This feature enables flexible and user-oriented error handling.Besides,ValidationResultprovides a methodor_else, which returns the validated instance if validation succeeded, otherwise invokes given function with the validation error.def handle_error(e):
    raise e

d = r.or_else(handle_error)Requirement phase+operator lets aValidatorfail if the input value is missing,Noneor considered to beempty.from dhampyr import *

class C:
    a: int = +v(default=0)

r = validate_dict(C, dict())

assert r.failures["a"].name == "missing"By default, Theemptycondition is applied to the input whose type isstrbyteslistorset. The input is considered empty when its length (returned bylen) is 0.  You can add conditions for other types by configuration interfaces.Although all of those 3 conditions must be satisfied by default, we sometimes need to change the behavior against each condition respectively. This can be done by bitwise operator and condition specifiers.operatorbehavior&Let validator fail when the next condition is not satisfied./Let validator continue to subsequent phases even when the next condition is not satisfied.^Let validator skip subsequent phases without failure when the next condition is not satisfied.Conditions forNoneandemptyare specified withNoneand...respectively.def longer5(x):
    return len(x) > 5

class C:
    a: str = +v(default="a")
    b: str = +v(..., longer5, default="b") ^ None
    c: str = +v(..., longer5, default="c") / ...
    d: str = +v(..., longer5, default="d") ^ ...

r = validate_dict(C, dict(a="", b=None, c="", d=""))
d = r.get()

assert r.failures["a"].name == "empty"
assert r.failures["b"] is None
assert r.failures["c"].name == "longer5"
assert r.failures["d"] is None
assert d.b == "b"
assert d.d == "d"The example shows that validation oncfailed at verification phase because/continues validation scheme to empty input.Conversion phaseConversion phase is done by aConveterwhich can be declared by multiple styles.specifierexamplenamebehaviorfunction or typeintname of the function or type.Invoke the function or constructor of the type.functools.partialpartial(int, base=2)name of base functionInvoke thepartialobject.tuple ofstrand another specifier("integer", int)first elementSame as the specifier at second element.enum.EnumtypeEname of the typeGet an enum value whose name matches the input.from functools import partial as p
from enum import Enum, auto

class D:
    a: int = v(default=0)

class E(Enum):
    E1 = auto()
    E2 = auto()

class C:
    a: int = v(default=0)
    b: int = v(p(int, base=2), default=0)
    c: str = v(("first", lambda x: x.split(",")[0]), default="")
    d: D = v()
    e: E = v(default=E.E1)

r = validate_dict(C, dict(a = "3", b = "101", c = "a,b,c", d = dict(a = "4"), e = "E2"))
d = r.get()

assert d.a == 3
assert d.b == 5
assert d.c == "a"
assert d.d.a == 4
assert d.e == E.E2Freezed arguments ofpartialfunction (forb,base=2) are passed to error object when the converter fails. They can be obtained viaargsorkwargsattribute ofValidationFailurefor the use of such as error message creation.Tuple style shown atcis available to give an explicit name to aConverter, especially for the case using lambda expression.Enumtype is also a type but it is treated in another way. TheConverterinvokes__getitem__class method to find an enum value by its name. Be sure that this method is case sensitive.As described in composite validation section, theConverterof iterable attribute interprets the input as iterable values and convert each value respectively, which is clarified in following code.class C:
    a: int = v(default=0)
    b: list[int] = v(default_factory=list)

r = validate_dict(C, dict(a="123", b="123"))

assert r.get().a == 123
assert r.get().b == [1, 2, 3]Verification phaseSimilar toConverter, there also are multiple declaration styles forVerifier.specifierexamplenamebehaviorfunction or typelt3name of the function or type.Invoke the function or constructor of the type.functools.partialpartial(lt, threshold = 3)name of base functionInvoke thepartialobject.tuple ofstrand another specifier("less_than_3", lt3)first elementSame as the specifier at second element.def lt3(x):
    return x < 3

def lt(x, threshold):
    return x < threshold

class C:
    a: int = v(..., lt3, default=0)
    b: int = v(..., p(lt, threshold = 3))
    c: int = v(..., ("less_than_3", lambda x: x < 3), default=0) = 0
    d: list[int] = v(..., [lt3], lambda x: len(x) < 5, default_factory=list)

r = validate_dict(C, dict(a=3, b=3, c=3, d=[1, 1, 1, 1, 1]))
assert {str(p) for p, _ in r.failures} == {"a", "b", "c", "d"}

r = validate_dict(C, dict(a=2, b=2, c=2, d=[1, 1, 1, 1]))
assert {str(p) for p, _ in r.failures} == {}These styles work similarly to equivalent style ofConverterspecifier. As for list expression ind, secondVerifieris not enclosed by[], so that it takes a list of converted values, not each value in the list. Therefore it fails when the length of the input list is not shorter than5.Verifier methodValidatable type is able to containverifier methods which are invoked at the end of verification phase.@validate()decorator marks a method asverifier method. Be aware that the bracket is necessary if no arguments are given.This decorator takes keyword arguments which represent dependencies determining whether the verifier method will be invoked. Each key of argument denotes an attribute name and the value is a boolean. If the value isTrue, the attribute haspositive dependency, otherwisenegative dependency.If no arguments are given, the verifier method is invoked only when all validations on attributes succeeded.If validations on all of attributes having positive dependency succeeded, the verifier method is invoked even when there are failed validations on other attributes.If validations on attributes having negative dependency failed, the verifier method is not invoked even when positive dependencies are satisfied.class C:
    a: int = +v()
    b: int = +v()
    c: int = +v()

    @validate()
    def v1(self):
        return self.a > 0

    @validate(a=True)
    def v2(self):
        return self.a > 0

    @validate(a=True, b=False)
    def v3(self):
        return self.a > 0

r = validate_dict(C, dict(a="0", b="0", c="0"))
assert {str(p) for p, _ in r.failures} == {"v1", "v2", "v3"}

r = validate_dict(C, dict(a="0", b="a", c="a"))
assert {str(p) for p, _ in r.failures} == {"b", "c", "v2"}
assert r.failures["v2"].name == "v2"

r = validate_dict(C, dict(a="0", b="0", c="a"))
assert {str(p) for p, _ in r.failures} == {"c", "v2", "v3"}
assert r.failures["v3"].name == "v3"Above code shows examples of verifier methods with various dependencies.v1has no dependencies so that it is executed only when validations of all attribute succeeded.v2is executed in every case because validation results onbandchave no concern. As forv3which has negative dependency onb, it is not executed in second case where the validation onbfails.As shown in the code, an error caused by a verifier method is stored on the path of its name, andnameof the error is also its name.VariableVerifiers can be declared by any kind ofcallables such as normal functions and lambda expressions. However, it is sometimes bothersome to define functions explicitly, and, lambda expression of python is somewhat verbose. To make things better in that point,dhampyrpackage exports a variable objectx.xis a variable which will be replaced with the input value, and various operations applied to it are evaluated lazily in verification phase.class C:
    a: int = v(..., x > 0)
    b: str = v(..., x.len % 2 == 0)
    c: int = v(..., x.in_(1, 2, 3))
    d: int = v(..., x.not_.in_(1, 2, 3))

r = validate_dict(C, dict(a=0, b="abc", c=0, d=1))

assert r.failures["a"].name == "x.gt"
assert r.failures["a"].kwargs == {"gt.value": 0}
assert r.failures["b"].name == "x.len.mod"
assert r.failures["b"].kwargs == {"mod.value": 2, "eq.value": 0}
assert r.failures["c"].name == "x.in"
assert r.failures["c"].kwargs == {"value": (1, 2, 3)}
assert r.failures["d"].name == "x.not.in"
assert r.failures["d"].kwargs == {"value": (1, 2, 3)}lenis a property which applies builtinlenfunction to the value, which is introduced because python specification restricts that__len__returns a value ofint.not_should be prepended to other operations and it inverts their result.When the verifier fails, it exposes the error whose name is concatenated operation names and which contains parameters of operations inkwargsattribute.Comparison operatorsoperatornameremarks<lt<=le==eq!=ne>=ge>gtMathematical binary operatorsoperatornameremarks+add-sub*mul@matmal/truediv//floordiv%mod**powMathematical unary operatorsoperatornameremarks-neg+pos~invert<<lshift>>rshift&and^xor|orMathematical functionsfunctionnameremarksdivmod()divmodBuiltin function.pow()powBuiltin function.abs()absBuiltin function.round()roundBuiltin function.math.trunc()truncFrommathpackage.math.floor()floorFrommathpackage.math.ceil()ceilFrommathpackage.Attributes and methodsattributenameremarks.not_notInvert results of subsequent operations..lenlenLength of the input value..invinvInvert result of previous operation..has()hasContains argument value or not?.in_()inBe contained in argument values?.x@xReturnxattribute.[x][x]Return value on indexx.Because this feature is added for the purpose of simplicity and intuitivity, it has some limitations listed below. Do not usexin these situations.xcan not appear multiple times in an equation.Logical combinations, which are expressed by operators such asor, are not available.When using the same operator multiple times, their parameters in the error object are overwritten by later one's.Validation contextvalidate_dicttakesValidationContextoptionally. Features of this object are listed below.Arbitrary values can be set to its attributes and they are available in conversion or verification function.Keeps key-value pairs which exist in the input but are not used in the validation scheme.Provides an interface to modify configurations which are effective under certain path.First of all,ValidationContextis the object used at each validation path. The state of the context can be set independently beforehand and the state propagates to descendant paths. Next example show how it works.context = ValidationContext()

context["a"].put(value=1)
context["b"].put(value=2)
context["a"][0].put(value=3)

def gt(x, cxt:ValidationContext):
    return x > cxt.value

class C:
    a: list[int] = v(..., [gt])
    b: int = v(..., gt)

r = validate_dict(C, dict(a=["2", "2"], b="2"), context)

assert {str(p) for p, _ in r.failures} == {"a[0]", "b"}Each value forais verified bygtwhich checks the input is greater thancxt.valuewhich is set to1byput.Only the first item inafails becausecxt.valueata[0]is set to3.As forb, verification fails becausecxt.valueis2.In order to use the context object inVerifierfunction, it should be declared to have an argument which is annotated withValidationContextlikelt. Context object is given only when the signature satisfies the format, which is the same forConterterfunction as well. Because the context is created on every path, the argument is always notNonebut the access to unset attribute raisesAttributeError.Undeclared keysvalidate_dictjust ignores items in input dictionary whose keys are not declared on the validatable type. Instead, they are kept inremaindersofValidationContextafter the validation. The context can be obtained fromValidationResultviacontextattribute even if you don't give a context explicitly. To get the undeclared values in nested types, use hierarchical access to the context.class D:
    d: int = v(default=0)

class C:
    a: int = v(default=0) = 0
    b: Optional[D] = v(default=None)
    c: list[D] = v(default_factory=list)

r = validate_dict(C, dict(a="1", b=dict(d="2", e="a"), c=[dict(d="3", e1="b"), dict(d="4", e2="c")], d="d"))
cxt = r.context

assert cxt.remainders == dict(d="d")
assert cxt["b"].remainders == dict(e="a")
assert cxt["c"][0].remainders == dict(e1="b")
assert cxt["c"][1].remainders == dict(e2="c")ConfigurationsThere are some configuration options which control the behavior of validations. Global configuration can be obtained bydefault_config. Meanwhile, configuration used at each path can be set viaconfigureofValidationContext.Due to the configuration object is also a context manager, it can be changed locally by usingwithblock.At runtime,configattribute ofValidationContextexposes configurations effective on the path.with default_config() as cfg:
    cfg.name = "modified"
    cfg.skip_null = False

    assert default_config().name is "modified"
    assert default_config().skip_null is False

assert default_config().name is "default"
assert default_config().skip_null is TrueSee the documentation ofValidationConfigurationto know all possible configurable parameters.