# -*- coding: utf-8 -*-
from setuptools import setup

packages = \
['koda_validate']

package_data = \
{'': ['*']}

install_requires = \
['koda==1.1.0']

setup_kwargs = {
    'name': 'koda-validate',
    'version': '1.0.0',
    'description': 'Typesafe, combinable validation',
    'long_description': '# Koda Validate\n\nTypesafe, combinable validation. Python 3.8+\n\nKoda Validate aims to make writing validators easier.\n\n### Install\npip\n```bash\npip install koda_validate\n```\nPoetry\n```bash\npoetry add koda_validate\n```\n\n## The Basics\n\n```python3\nfrom dataclasses import dataclass\nfrom koda import Ok\nfrom koda_validate import *\n\n\n@dataclass\nclass Person:\n    name: str\n    age: int\n\n\nperson_validator = dict_validator(\n    Person, \n    key("name", StringValidator()),\n    key("age", IntValidator())\n)\n\nresult = person_validator({"name": "John Doe", "age": 30})\nif isinstance(result, Ok):\n    print(f"{result.val.name} is {result.val.age} years old")\nelse:\n    print(result.val)\n```\n\nWe could also nest `person_validator`, for instance, in a `ListValidator`:\n```python\npeople_validator = ListValidator(person_validator)\n```\n...And nest that in a different validator (and so forth...):\n```python\n\n@dataclass\nclass Group:\n    name: str\n    people: list[Person]\n\n\ngroup_validator = dict_validator(\n    Group,\n    key("name", StringValidator()),\n    key("people", people_validator),\n)\n\ndata = {\n    "name": "Arrested Development Characters",\n    "people": [\n        {"name": "George Bluth", "age": 70},\n        {"name": "Michael Bluth", "age": 35}\n    ]\n}\n\nassert group_validator(data) == Ok(\n    Group(\n        name=\'Arrested Development Characters\',\n        people=[\n            Person(name=\'George Bluth\', age=70),\n            Person(name=\'Michael Bluth\', age=35)\n        ]\n    )\n)\n```\n\nLet\'s look at the `dict_validator` a bit closer. Its first argument can be any `Callable` that accepts the values from \neach key below it -- in the same order they are defined (the names of the keys and the `Callable` arguments do not need \nto match). For `person_validator`, we used a `Person` `dataclass`; for `Group`, we used a `Group` dataclass; but that \ndoes not need to be the case. Because we can use any `Callable` with matching types, this would also be valid:\n```python\nfrom koda import Ok\nfrom koda_validate import *\n\n\ndef reverse_person_args_tuple(a: str, b: int) -> tuple[int, str]:\n    return b, a\n\nperson_validator_2 = dict_validator(\n    reverse_person_args_tuple,\n    key("name", StringValidator()),\n    key("age", IntValidator()),\n)\n\nassert person_validator_2({"name": "John Doe", "age": 30}) == Ok((30, "John Doe"))\n\n```\nAs you see, we have some flexibility in defining what we want to get back from a validated `dict_validator`. \n\nAnother thing to note is that, so far, the results are all wrapped in an `Ok` class. The other possibility -- when \nvalidation fails -- is that an `Err` instance is returned, with whatever message or object a given validator returns. \nWe do not raise exceptions to express validation failure in Koda Validate. Instead, validation is treated as part of \nnormal control flow.\n\nLet\'s use some more features.\n\n```python\nfrom dataclasses import dataclass\nfrom koda import Err, Ok, Result\nfrom koda_validate import *\n\n\n@dataclass\nclass Employee:\n    title: str\n    name: str\n\n\ndef no_dwight_regional_manager(employee: Employee) -> Result[Employee, Serializable]:\n    if (\n        "schrute" in employee.name.lower()\n        and employee.title.lower() == "assistant regional manager"\n    ):\n        return Err("Assistant TO THE Regional Manager!")\n    else:\n        return Ok(employee)\n\n\nemployee_validator = dict_validator(\n    Employee,\n    key("title", StringValidator(not_blank, MaxLength(100), preprocessors=[strip])),\n    key("name", StringValidator(not_blank, preprocessors=[strip])),\n    # After we\'ve validated individual fields, we may want to validate them as a whole\n    validate_object=no_dwight_regional_manager,\n)\n\n\n# The fields are valid but the object as a whole is not.\nassert employee_validator(\n    {\n        "title": "Assistant Regional Manager",\n        "name": "Dwight Schrute",\n    }\n) == Err("Assistant TO THE Regional Manager!")\n\n```\nThings to note about `employee_validator`:\n- we can add additional checks -- `Predicate`s -- to validators (e.g. `not_blank`, `MaxLength`, etc.)\n- we can pre-process strings for formatting (after the type is determined, but before `Predicate` validators are run)\n- we have two stages of validation on dictionaries: first the keys, then the entire object, via `validate_object`\n- apparently we have a problem with someone named Dwight Schrute giving himself the wrong title\n\n\nNote that everything we\'ve seen is typesafe according to mypy -- with strict settings, and without any plugins.\n\n### The (More) Basics\n\nWe\'re are spending a lot of time discussing validating collections, but Koda Validator works just as well on simple \nvalues.\n\n```python\nfrom koda import Err, Ok\nfrom koda_validate import ExactValidator, MinLength, StringValidator\n\nmin_length_3_validator = StringValidator(MinLength(4))\nassert min_length_3_validator("good") == Ok("good")\nassert min_length_3_validator("bad") == Err(["minimum allowed length is 4"])\n\nexactly_5_validator = ExactValidator(5)\n\nassert exactly_5_validator(5) == Ok(5)\nassert exactly_5_validator("hmm") == Err(["expected exactly 5 (int)"])\n\n```\nKoda Validate is intended to be extendable enough to validate any type of data.\n\n## Validation Errors\n\nAs mentioned above, errors are returned as data as part of normal control flow. The contents of all returned `Err`s \nfrom built-in validators in Koda Validate are JSON/YAML serializable. (However, should you build your own custom \nvalidators, there is no contract enforcing that constraint.) Here are a few examples of the kinds of errors you can \nexpect to see out of the box.\n\n```python\nfrom dataclasses import dataclass\nfrom koda import Err, Maybe\nfrom koda_validate import *\n\n# Wrong type\nassert StringValidator()(None) == Err(["expected a string"])\n\n# All failing `Predicate`s are reported (not just the first)\nstr_choice_validator = StringValidator(MinLength(2), Choices({"abc", "yz"}))\nassert str_choice_validator("") == Err(\n    ["minimum allowed length is 2", "expected one of [\'abc\', \'yz\']"]\n)\n\n\n@dataclass\nclass City:\n    name: str\n    region: Maybe[str]\n\n\ncity_validator = dict_validator(\n    City,\n    key("name", StringValidator(not_blank)),\n    maybe_key("region", StringValidator(not_blank)),\n)\n\n# We use the key "__container__" for object-level errors\nassert city_validator(None) == Err({"__container__": ["expected a dictionary"]})\n\n# Missing keys are errors\nassert city_validator({}) == Err({"name": ["key missing"]})\n\n# Extra keys are also errors\nassert city_validator(\n    {"region": "California", "population": 510, "country": "USA"}\n) == Err({"__container__": ["Received unknown keys. Only expected [\'name\', \'region\']"]})\n\n\n@dataclass\nclass Neighborhood:\n    name: str\n    city: City\n\n\nneighborhood_validator = dict_validator(\n    Neighborhood, key("name", StringValidator(not_blank)), key("city", city_validator)\n)\n\n# Errors are nested in predictable manner\nassert neighborhood_validator({"name": "Bushwick", "city": {}}) == Err(\n    {"city": {"name": ["key missing"]}}\n)\n\n```\nIf you have any concerns about being able to handle specific types of key or object requirements, please see some of \nthe other validators and helpers below:\n- [OneOf2 / OneOf3](#oneof2--oneof3)\n- [MapValidator](#mapvalidator)\n- [OptionalValidator](#optionalvalidator)\n- [maybe_key](#maybe_key)\n- [is_dict_validator](#is_dict_validator)\n- [Lazy](#lazy)\n\n\n## Validators, Predicates, and Extension\nKoda Validate\'s intention is to cover the bulk of common use cases with its built-in tools. However, it is also meant \nto provide a straightforward way to build for custom validation use-cases. Here we\'ll provide a quick overview of how \ncustom validation logic can be implemented.\n\nThere are two kinds of `Callable`s used for validation in Koda Validate: `Validator`s and `Predicate`s. `Validator`s \ncan take an input of one type and produce a valid result of another type. (While a `Validator` has the capability to \nalter a value and/or type, whether it does is entirely dependent on the given `Validator`s requirements.) Most commonly \n`Validator`s accept type `Any` and validate that it conforms to some type or data shape. As an example, we\'ll \nwrite a simple `Validator` for `float`s here:\n\n```python\nfrom typing import Any\nfrom koda import Err, Ok, Result\nfrom koda_validate.typedefs import Serializable, Validator\n\n\nclass SimpleFloatValidator(Validator[Any, float, Serializable]):\n    def __call__(self, val: Any) -> Result[float, Serializable]:\n        if isinstance(val, float):\n            return Ok(val)\n        else:\n            return Err("expected a float")\n\n\nfloat_validator = SimpleFloatValidator()\nfloat_val = 5.5\nassert float_validator(float_val) == Ok(float_val)\nassert float_validator(5) == Err("expected a float")\n```\n\nWhat is this doing? \n- extending `Validator`, using the following types:\n  - `Any`: any type of input can be passed in\n  - `float`: if the data is valid, a value of type `Ok[float]` will be returned \n  - `Serializable`: if it\'s invalid, a value of type `Err[Serializable]` will be returned\n- the `__call__` method performs any kind of validation needed, so long as the input and output type signatures -- as determined by the `Validator` type parameters - are abided\n\nWe accept `Any` because the type of input may be unknown before submitting to the `Validator`. After our \nvalidation in `SimpleFloatValidator` succeeds, we know the type must be `float`.   \n\nThis is all well and good, but we\'ll probably want to be able to validate against values of the floats, such as min, \nmax, or rough equality checks. For this we use `Predicate`s. This is what the `FloatValidator` in Koda Validate looks \nlike:\n\n```python\nclass FloatValidator(Validator[Any, float, Serializable]):\n    def __init__(self, *predicates: Predicate[float, Serializable]) -> None:\n        self.predicates = predicates\n\n    def __call__(self, val: Any) -> Result[float, Serializable]:\n        if isinstance(val, float):\n            return accum_errors_serializable(val, self.predicates)\n        else:\n            return Err(["expected a float"])\n\n```\n\n`Predicate`s are meant to validate the _value_ of a known type -- as opposed to validating at the type-level. For \nexample, this is how you might write and use a `Predicate` for approximate `float` equality:\n\n```python\nimport math\nfrom dataclasses import dataclass\nfrom koda import Err, Ok\nfrom koda_validate import FloatValidator, Serializable, Predicate\n\n\n@dataclass\nclass IsClose(Predicate[float, Serializable]):\n    compare_to: float\n    tolerance: float\n\n    def is_valid(self, val: float) -> bool:\n        return math.isclose(self.compare_to, val, abs_tol=self.tolerance)\n\n    def err(self, val: float) -> Serializable:\n        return f"expected a value within {self.tolerance} of {self.compare_to}"\n\n\n# let\'s use it\nclose_to_validator = FloatValidator(IsClose(0.05, 0.02))\na = 0.06\nassert close_to_validator(a) == Ok(a)\nassert close_to_validator(0.01) == Err(["expected a value within 0.02 of 0.05"])\n\n```\n\nNotice that in `Predicate`s we define `is_valid` and `err` methods, while in `Validator`s we define the \nentire `__call__` method. This is because the base `Predicate` class is constructed in such a way that we limit how \nmuch it can actually do -- we don\'t want it to be able to alter the value being validated. This turns out to be useful \nbecause it allows us to proceed sequentially through an arbitrary amount of `Predicate`s of the same type in a given \n`Validator`. Only because of this property can we be confident in our ability to return all `Predicate` errors for a \ngiven `Validator` -- instead of having to exit at the first failure.\n\n## Metadata\nPreviously we said an aim of Koda Validate is to allow reuse of validator metadata. Principally this \nis useful in generating descriptions of the validator\'s constraints -- one example could be generating\nan OpenAPI (or other) schema. Here we\'ll do something simpler and use validator metadata to build a function which can \nreturn plaintext descriptions of validators:\n\n```python3\nfrom typing import Any\nfrom koda_validate import MaxLength, MinLength, Predicate, StringValidator, Validator\n\n\ndef describe_validator(validator: Validator[Any, Any, Any] | Predicate[Any, Any]) -> str:\n    # use `isinstance(...)` in python <= 3.10\n    match validator:\n        case StringValidator(predicates):\n            predicate_descriptions = [\n                f"- {describe_validator(pred)}" for pred in predicates\n            ]\n            return "\\n".join(["validates a string"] + predicate_descriptions)\n        case MinLength(length):\n            return f"minimum length {length}"\n        case MaxLength(length):\n            return f"maximum length {length}"\n        # ...etc\n        case _:\n            raise TypeError(f"unhandled validator type. got {type(validator)}")\n\n\nprint(describe_validator(StringValidator()))\n# validates a string\nprint(describe_validator(StringValidator(MinLength(5))))\n# validates a string\n# - minimum length 5\nprint(describe_validator(StringValidator(MinLength(3), MaxLength(8))))\n# validates a string\n# - minimum length 3\n# - maximum length 8\n\n```\nAll we\'re doing here, of course, is writing an interpreter. For the sake of brevity this one is very simple, but it\'s\nstraightforward to extend the logic. This is easy to do because, while the validators are `Callable`s at their \ncore, they are also classes that can easily be inspected. (This ease of inspection is the primary reason we use\nclasses in Koda Validate.) Interpreters are the recommended way to re-use validator metadata for \nnon-validation purposes.\n\n## Other Noteworthy Validators and Utilities \n\n\n#### OneOf2 / OneOf3\n\nOneOfN validators are useful when you may have multiple valid shapes of data.\n```python\nfrom koda import First, Ok, Second\n\nfrom koda_validate import ListValidator, OneOf2, StringValidator\n\nstring_or_list_string_validator = OneOf2(\n    StringValidator(), ListValidator(StringValidator())\n)\n\nassert string_or_list_string_validator("ok") == Ok(First("ok"))\nassert string_or_list_string_validator(["list", "of", "strings"]) == Ok(\n    Second(["list", "of", "strings"])\n)\n```\n\n\n#### Tuple2Validator / Tuple3Validator\n\nThese `Validator`s work on `tuple`s as you might expect:\n```python\nfrom koda import Ok\nfrom koda_validate import IntValidator, StringValidator, Tuple2Validator\n\nstring_int_validator = Tuple2Validator(StringValidator(), IntValidator())\n\nassert string_int_validator(("ok", 100)) == Ok(("ok", 100))\n\n# also ok with lists\nassert string_int_validator(["ok", 100]) == Ok(("ok", 100))\n\n```\n\n\n#### Lazy\n`Lazy`\'s main purpose is to allow for the use of recursion in validation. An example use case of this might be replies\nin a comment thread. This can be done with mutually recursive functions. For simplicity, here\'s an example of parsing a \nkind of non-empty list.\n\n```python\nfrom typing import Optional\nfrom koda import Ok\nfrom koda_validate import IntValidator, Lazy, OptionalValidator, Tuple2Validator\n\nNonEmptyList = tuple[int, Optional["NonEmptyList"]]\n\n\ndef recur_non_empty_list() -> Tuple2Validator[int, Optional[NonEmptyList]]:\n    return non_empty_list_validator\n\n\nnon_empty_list_validator = Tuple2Validator(\n    IntValidator(),\n    OptionalValidator(Lazy(recur_non_empty_list)),\n)\n\nassert non_empty_list_validator((1, (1, (2, (3, (5, None)))))) == Ok(\n    (1, (1, (2, (3, (5, None)))))\n)\n\n```\n\n\n#### MapValidator\n\n`MapValidator` allows us to validate dictionaries that are mappings of one type to another type, where we don\'t\nneed to be concerned about individual keys or values:\n\n```python\nfrom koda import Ok\nfrom koda_validate import IntValidator, MapValidator, StringValidator\n\nstr_to_int_validator = MapValidator(StringValidator(), IntValidator())\n\nassert str_to_int_validator({"a": 1, "b": 25, "xyz": 900}) == Ok(\n    {"a": 1, "b": 25, "xyz": 900}\n)\n\n```\n\n#### OptionalValidator\n\n`OptionalValidator` is very simple. It validates a value is either `None` or passes another validator\'s rules.\n\n```python\nfrom koda import Ok\nfrom koda_validate import IntValidator, OptionalValidator\n\noptional_int_validator = OptionalValidator(IntValidator())\n\nassert optional_int_validator(5) == Ok(5)\nassert optional_int_validator(None) == Ok(None)\n\n```\n\n#### maybe_key\n`maybe_key` allows for a key to be missing from a dictionary\n\n```python\nfrom dataclasses import dataclass\nfrom koda import Just, Maybe, Ok, nothing\nfrom koda_validate import IntValidator, StringValidator, dict_validator, key, maybe_key\n\n\n@dataclass\nclass Person:\n    name: str\n    age: Maybe[int]\n\n\nperson_validator = dict_validator(\n    Person, key("name", StringValidator()), maybe_key("age", IntValidator())\n)\nassert person_validator({"name": "Bob"}) == Ok(Person("Bob", nothing))\nassert person_validator({"name": "Bob", "age": 42}) == Ok(Person("Bob", Just(42)))\n\n```\n\n#### is_dict_validator\n\nA very simple validator that only validates that and object is a dict. It doesn\'t do any validation against keys or\nvalues.\n\n```python\nfrom koda import Ok, Err\nfrom koda_validate.dictionary import is_dict_validator\n\nassert is_dict_validator({}) == Ok({})\nassert is_dict_validator(None) == Err({"__container__": ["expected a dictionary"]})\nassert is_dict_validator({"a": 1, "b": 2, 5: "xyz"}) == Ok(\n    {"a": 1, "b": 2, 5: "xyz"}\n)\n\n```\n\n## Limitations\n\n#### `dict_validator` has a max keys limit\n\nBy default `dict_validator` can have a maximum of 20 keys. You can change this by generating code\nand storing it in your project:\n```bash\n# allow up to 30 keys\npython /path/to/koda-validate/codegen/generate.py /your/target/directory --num-keys 30\n```\nThis limitation exists because computation starts to get expensive for type checkers above a certain level, and \nit\'s not common to have that many keys in a dict.\n\n\n#### `dict_validator` types may be hard to read / slow for your editor or type-checker\n\n`dict_validator` is a convenience function that delegates to different `Validator`s depending \non the number of keys -- for example, `Dict2KeysValidator`, `Dict3KeysValidator`, etc. These\nnumbered validators are limited to a specific number of keys and can be used to mitigate\nsuch issues.\n\n\n#### `dict_validator`\'s keys only allow for strings\n\nThis should be resolved in a later release.\n\n\n#### Your Imagination\n:sparkles:\n\n\n### Something\'s Missing Or Wrong \nOpen an [issue on GitHub](https://github.com/keithasaurus/koda-validate/issues) please!\n',
    'author': 'Keith Philpott',
    'author_email': 'None',
    'maintainer': 'None',
    'maintainer_email': 'None',
    'url': 'https://github.com/keithasaurus/koda-validate',
    'packages': packages,
    'package_data': package_data,
    'install_requires': install_requires,
    'python_requires': '>=3.8,<4.0',
}


setup(**setup_kwargs)
