{ "info": { "author": "Mark Vartanyan", "author_email": "kolypto@gmail.com", "bugtrack_url": null, "classifiers": [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python :: 2", "Programming Language :: Python :: 3", "Topic :: Software Development :: Libraries :: Python Modules" ], "description": "[![Build Status](https://api.travis-ci.org/kolypto/py-good.png?branch=master)](https://travis-ci.org/kolypto/py-good)\n[![Pythons](https://img.shields.io/badge/python-3.5%E2%80%933.8%20%7C%20pypy3-blue.svg)](.travis.yml)\n\n\n\n\n\n\n\nGood\n====\n\nSlim yet handsome validation library.\n\nCore features:\n\n* Simple\n* Customizable\n* Supports nested model validation\n* Error paths (which field contains the error)\n* User-friendly error messages\n* Internationalization!\n* [Robust](misc/performance/performance.md): 10 000 validations per second\n* Python 2.7, 3.3+ compatible\n* 100% documented and unit-tested\n\nInspired by the amazing [alecthomas/voluptuous](https://github.com/alecthomas/voluptuous) and 100% compatible with it.\nThe whole internals have been reworked towards readability and robustness. And yeah, the docs are now exhaustive :)\n\nThe rationale for a remake was to make it modular with a tiny core and everything else built on top of that,\nensure that all error messages are user-friendly out of the box, and tweak the performance.\n\n\nTable of Contents\n=================\n\n* Voluptuous Drop-In Replacement\n* Schema\n * Callables\n * Priorities\n * Creating a Schema\n * Validating\n* Errors\n * Invalid\n * Invalid.enrich()\n * MultipleInvalid\n* Markers\n * Required\n * Optional\n * Remove\n * Reject\n * Allow\n * Extra\n * Entire\n* Validation Tools\n * Helpers\n * Object\n * Msg\n * Test\n * message\n * name\n * truth\n * Predicates\n * Maybe\n * Any\n * All\n * Neither\n * Inclusive\n * Exclusive\n * Types\n * Type\n * Coerce\n * Values\n * In\n * Length\n * Default\n * Fallback\n * Map\n * Boolean\n * Check\n * Truthy\n * Falsy\n * Boolean\n * Numbers\n * Range\n * Clamp\n * Strings\n * Lower\n * Upper\n * Capitalize\n * Title\n * Match\n * Replace\n * Url\n * Email\n * Dates\n * DateTime\n * Date\n * Time\n * Files\n * IsFile\n * IsDir\n * PathExists\n\n\nVoluptuous Drop-In Replacement\n==============================\n\nDespite Good is modelled after Voluptuous and is highly compatible,\nthere still are differences that would definitely break your project.\n\nIf you're not ready for such a change -- `good.voluptuous` is the solution:\ncompatibility layer for switching from [voluptuous 0.8.5](https://github.com/alecthomas/voluptuous)\nwith 100% compatibility.\n\nThis is a drop-in replacement that passes all voluptuous unit-tests and hence should work perfectly.\nHere's how to use it\n\n```python\n#from voluptuous import * # no more\nfrom good.voluptuous import * # replacement\n\n# .. and use it like before\n```\n\nIncludes all the features and is absolutely compatible, except for the error message texts,\nwhich became much more user-friendly :)\n\nMigration steps:\n\n1. Replace `voluptuous` imports with `good.voluptuous`\n2. Run your application tests and see how it behaves\n3. Module by module, replace `good.voluptuous` with just `good`, keeping the differences in mind.\n\nAlso note the small differences that are still present:\n\n* Settings for `required` and `extra` are not inherited by embedded mappings.\n\n If your top-level schema defines `required=False`, embedded mappings will still have the default `required=True`!\n And same with `extra`.\n\n* Different error message texts, which are easier to understand :)\n* Raises `Invalid` rather than `MultipleInvalid` for rejected extra mapping keys (see [`Extra`](#extra))\n\nGood luck! :)\n\nSchema\n======\n\nValidation schema.\n\nA schema is a Python structure where nodes are pattern-matched against the corresponding values.\nIt leverages the full flexibility of Python, allowing you to match values, types, data structures and much more.\n\nWhen a schema is created, it's compiled into a callable function which does the validation, hence it does not need\nto analyze the schema every time.\n\nOnce the Schema is defined, validation can be triggered by calling it:\n\n```python\nfrom good import Schema\n\nschema = Schema({ 'a': str })\n# Test\nschema({ 'a': 'i am a valid string' })\n```\n\nThe following rules exist:\n\n1. **Literal**: plain value is validated with direct comparison (equality check):\n\n ```python\n Schema(1)(1) #-> 1\n Schema(1)(2) #-> Invalid: Invalid value: expected 1, got 2\n ```\n\n2. **Type**: type schema produces a strict `type(v) == schema` check on the input value:\n\n ```python\n Schema(int)(1) #-> 1\n Schema(int)(True)\n #-> Invalid: Wrong type: expected Integer number, got Boolean\n Schema(int)('1')\n #-> Invalid: Wrong type: expected Integer number, got Binary String\n ```\n\n For Python2, there is an exception for `basestring`: it won't make strict type checks, but rather `isinstance()`.\n\n For a relaxed `isinstance()` check, see [`Type`](#type) validator.\n\n3. **Enum**:\n [Python 3.4 Enums](https://docs.python.org/3/library/enum.html),\n or the backported [enum34](https://pypi.python.org/pypi/enum34).\n\n Tests whether the input value is a valid `Enum` value:\n\n ```python\n from enum import Enum\n\n class Colors(Enum):\n RED = 0xFF0000\n GREEN = 0x00FF00\n BLUE = 0x0000FF\n\n schema = Schema(Colors)\n\n schema(0xFF0000) #-> \n schema(Colors.RED) #-> \n schema(123)\n #-> Invalid: Invalid Colors value, expected Colors, got 123\n ```\n\n Output is always an instance of the provided `Enum` type value.\n\n4. **Callable**: is applied to the value and the result is used as the final value.\n\n Callables should raise [`Invalid`](#invalid) errors in case of a failure, however some generic error types are\n converted automatically: see [Callables](#callables).\n\n In addition, validators are allowed to transform a value to the required form.\n For instance, [`Coerce(int)`](#coerce) returns a callable which will convert input values into `int` or fail.\n\n ```python\n def CoerceInt(v): # naive Coerce(int) implementation\n return int(v)\n\n Schema(CoerceInt)(1) #-> 1\n Schema(CoerceInt)('1') #-> 1\n Schema(CoerceInt)('a')\n #-> Invalid: invalid literal for int(): expected CoerceInt(), got a\n ```\n\n5. **`Schema`**: a schema may contain sub-schemas:\n\n ```python\n sub_schema = Schema(int)\n schema = Schema([None, sub_schema])\n\n schema([None, 1, 2]) #-> [None, 1, 2]\n schema([None, '1']) #-> Invalid: invalid value\n ```\n\n Since `Schema` is callable, validation transparently by just calling it :)\n\nMoreover, instances of the following types are converted to callables on the compilation phase:\n\n1. **Iterables** (`list`, `tuple`, `set`, custom iterables):\n\n Iterables are treated as a set of valid values,\n where each value in the input is compared against each value in the schema.\n\n In order for the input to be valid, it needs to have the same iterable type, and all of its\n values should have at least one matching value in the schema.\n\n ```python\n schema = Schema([1, 2, 3]) # List of valid values\n\n schema([1, 2, 2]) #-> [1, 2, 2]\n schema([1, 2, 4]) #-> Invalid: Invalid value @ [2]: expected List[1|2|3], got 4\n schema((1, 2, 2)) #-> Invalid: Wrong value type: expected List, got Tuple\n ```\n\n Each value within the iterable is a schema as well, and validation requires that\n each member of the input value matches *any* of the schemas.\n Thus, an iterable is a way to define *OR* validation rule for every member of the iterable:\n\n ```python\n Schema([ # All values should be\n # .. int ..\n int,\n # .. or a string, casted to int ..\n lambda v: int(v)\n ])([ 1, 2, '3' ]) #-> [ 1, 2, 3 ]\n ```\n\n This example works like this:\n\n 1. Validate that the input value has the matching type: `list` in this case\n 2. For every member of the list, test that there is a matching value in the schema.\n\n E.g. for value `1` -- `int` matches (immediate `instanceof()` check).\n However, for value `'3'` -- `int` fails, but the callable manages to do it with no errors,\n and transforms the value as well.\n\n Since lists are ordered, the first schema that didn't fail is used.\n\n2. **Mappings** (`dict`, custom mappings):\n\n Each key-value pair in the input mapping is validated against the corresponding schema pair:\n\n ```python\n Schema({\n 'name': str,\n 'age': lambda v: int(v)\n })({\n 'name': 'Alex',\n 'age': '18',\n }) #-> {'name': 'Alex', 'age': 18}\n ```\n\n When validating, *both* keys and values are schemas, which allows to use nested schemas and interesting validation rules.\n For instance, let's use [`In`](#in) validator to match certain keys:\n\n ```python\n from good import Schema, In\n\n Schema({\n # These two keys should have integer values\n In({'age', 'height'}): int,\n # All other string keys (other than 'age', 'height') should have string values\n All(str, Neither(In({'age', 'height'}))): str,\n })({\n 'age': 18,\n 'height': 173,\n 'name': 'Alex',\n })\n ```\n\n This works like this:\n\n 1. Test that the input has a matching type (`dict`)\n 2. For each key in the input mapping, matching keys are selected from the schema\n 3. Validate input values with the corresponding value in the schema.\n\n In addition, certain keys can be marked as [`Required`](#required) and [`Optional`](#optional).\n The default behavior is to have all keys required, but this can be changed by providing\n `default_keys=Optional` argument to the Schema.\n\n Finally, a mapping does not allow any extra keys (keys not defined in the schema). To change this, provide\n `extra_keys=Allow` to the `Schema` constructor.\n\n Please note that `default_keys` and `extra_keys` settings do not propagate to sub-schemas and are only applied\n to the top-level mapping. If required, wrap sub-schemas with another `Schema()` and feed the settings, or\n use [Markers](#markers) explicitly.\n\nThese are just the basic rules, and for sure `Schema` can do much more than that!\nAdditional logic is implemented through [Markers](#markers) and [Validators](#validation-tools),\nwhich are described in the following chapters.\n\n## Callables\n\nFinally, here are the things to consider when using custom callables for validation:\n\n* Throwing errors.\n\n If the callable throws [`Invalid`](#invalid) exception, it's used as is with all the rich info it provides.\n Schema is smart enough to fill into most of the arguments (see [`Invalid.enrich`](#invalidenrich)),\n so it's enough to use a custom message, and probably, set a human-friendly `expected` field.\n\n In addition, specific error types are wrapped into `Invalid` automatically: these are\n `AssertionError`, `TypeError`, `ValueError`.\n Schema tries to do its best, but such messages will probably be cryptic for the user.\n Hence, always raise meaningful errors when creating custom validators.\n Still, this opens the possibility to use Python typecasting with validators like `lambda v: int(v)`,\n since most of them are throwing `TypeError` or `ValueError`.\n\n* Naming.\n\n If the provided callable does not specify `Invalid.expected` expected value,\n the `__name__` of the callable is be used instead.\n E.g. `def intify(v):pass` becomes `'intify()'` in reported errors.\n\n If a custom name is desired on the callable -- set the `name` attribute on the callable object.\n This works best with classes, however a function can accept `name` attribute as well.\n\n For convenience, [`@message`](#message) and [`@name`](#name) decorators can be used on callables\n to specify the name and override the error message used when the validator fails.\n\n* Signals.\n\n A callable may decide that the value is soooo invalid that it should be dropped from the sanitized output.\n In this case, the callable should raise `good.schema.signals.RemoveValue`.\n\n This is used by the `Remove()` marker, but can be leveraged by other callables as well.\n\n## Priorities\n\nEvery schema type has a priority ([source](good/schema/util.py)),\nwhich define the sequence for matching keys in a mapping schema:\n\n1. Literals have highest priority\n2. Types has lower priorities than literals, hence schemas can define specific rules for individual keys,\n and then declare general rules by type-matching:\n\n ```python\n Schema({\n 'name': str, # Specific rule with a literal\n str: int, # General rule with a type\n })\n ```\n3. Callables, iterables, mappings -- have lower priorities.\n\nIn addition, [Markers](#markers) have individual priorities,\nwhich can be higher that literals ([`Remove()`](#remove) marker) or lower than callables ([`Extra`](#extra) marker).\n\nCreating a Schema\n-----------------\n```python\nSchema(schema, default_keys=None, extra_keys=None)\n```\n\nCreates a compiled `Schema` object from the given schema definition.\n\nUnder the hood, it uses `SchemaCompiler`: see the [source](good/schema/compiler.py) if interested.\n\nArguments:\n\n* `schema`: Schema definition\n* `default_keys`: Default mapping keys behavior:\n a [`Marker`](#markers) class used as a default on mapping keys which are not Marker()ed with anything.\n\n Defaults to `markers.Required`.\n* `extra_keys`: Default extra keys behavior: sub-schema, or a [`Marker`](#markers) class.\n\n Defaults to `markers.Reject`\n\n\n\nThrows:\n\n* `SchemaError`: Schema compilation error\n\n\nValidating\n----------\n\n```python\nSchema.__call__(value)\n```\n\nHaving a [`Schema`](#schema), user input can be validated by calling the Schema on the input value.\n\nWhen called, the Schema will return sanitized value, or raise exceptions.\n\nArguments:\n\n* `value`: Input value to validate\n\nReturns: `None` Sanitized value\n\nThrows:\n\n* `good.MultipleInvalid`: Validation error on multiple values. See [`MultipleInvalid`](#multipleinvalid).\n* `good.Invalid`: Validation error on a single value. See [`Invalid`](#invalid).\n\n\nErrors\n======\n\nSource: [good/schema/errors.py](good/schema/errors.py)\n\nWhen [validating user input](#validating), [`Schema`](#schema) collects all errors and throws these\nafter the whole input value is validated. This makes sure that you can report *all* errors at once.\n\nWith simple schemas, like `Schema(int)`, only a single error is available: e.g. wrong value type.\nIn this case, [`Invalid`](#invalid) error is raised.\n\nHowever, with complex schemas with embedded structures and such, multiple errors can occur:\nthen [`MultipleInvalid`] is reported.\n\nAll errors are available right at the top-level:\n\n```python\nfrom good import Invalid, MultipleInvalid\n```\n\n## Invalid\n```python\nInvalid(message, expected=None, provided=None, path=None,\n validator=None, **info)\n```\n\nValidation error for a single value.\n\nThis exception is guaranteed to contain text values which are meaningful for the user.\n\nArguments:\n\n* `message`: Validation error message.\n* `expected`: Expected value: info about the value the validator was expecting.\n\n If validator does not specify it -- the name of the validator is used.\n* `provided`: Provided value: info about the value that was actually supplied by the user\n\n If validator does not specify it -- the input value is typecasted to string and stored here.\n* `path`: Path to the error value.\n\n E.g. if an invalid value was encountered at ['a'].b[1], then path=['a', 'b', 1].\n* `validator`: The validator that has failed: a schema item\n* `**info`: Custom values that might be provided by the validator. No built-in validator uses this.\n\n\n\n\n\n### `Invalid.enrich()`\n```python\nInvalid.enrich(expected=None, provided=None, path=None,\n validator=None)\n```\n\nEnrich this error with additional information.\n\nThis works with both Invalid and MultipleInvalid (thanks to `Invalid` being iterable):\nin the latter case, the defaults are applied to all collected errors.\n\nThe specified arguments are only set on `Invalid` errors which do not have any value on the property.\n\nOne exclusion is `path`: if provided, it is prepended to `Invalid.path`.\nThis feature is especially useful when validating the whole input with multiple different schemas:\n\n```python\nfrom good import Schema, Invalid\n\nschema = Schema(int)\ninput = {\n 'user': {\n 'age': 10,\n }\n}\n\ntry:\n schema(input['user']['age'])\nexcept Invalid as e:\n e.enrich(path=['user', 'age']) # Make the path reflect the reality\n raise # re-raise the error with updated fields\n```\n\nThis is used when validating a value within a container.\n\nArguments:\n\n* `expected`: Invalid.expected default\n* `provided`: Invalid.provided default\n* `path`: Prefix to prepend to Invalid.path\n* `validator`: Invalid.validator default\n\nReturns: `Invalid|MultipleInvalid` \n\n\n\n## MultipleInvalid\n```python\nMultipleInvalid(errors)\n```\n\nValidation errors for multiple values.\n\nThis error is raised when the [`Schema`](#schema) has reported multiple errors, e.g. for several dictionary keys.\n\n`MultipleInvalid` has the same attributes as [`Invalid`](#invalid),\nbut the values are taken from the first error in the list.\n\nIn addition, it has the `errors` attribute, which is a list of [`Invalid`](#invalid) errors collected by the schema.\nThe list is guaranteed to be plain: e.g. there will be no underlying hierarchy of `MultipleInvalid`.\n\nNote that both `Invalid` and `MultipleInvalid` are iterable, which allows to process them in singularity:\n\n```python\ntry:\n schema(input_value)\nexcept Invalid as ee:\n reported_problems = {}\n for e in ee: # Iterate over `Invalid`\n path_str = u'.'.join(e.path) # 'a.b.c.d', JavaScript-friendly :)\n reported_problems[path_str] = e.message\n #.. send reported_problems to the user\n```\n\nIn this example, we create a dictionary of paths (as strings) mapped to error strings for the user.\n\nArguments:\n\n* `errors`: The reported errors.\n\n If it contains `MultipleInvalid` errors -- the list is recursively flattened\n so all of them are guaranteed to be instances of [`Invalid`](#invalid).\n\n\n\n\n\n\n\n\n\n\n\n\nMarkers\n=======\nA *Marker* is a proxy class which wraps some schema.\n\nImmediately, the example is:\n\n```python\nfrom good import Schema, Required\n\nSchema({\n 'name': str, # required key\n Optional('age'): int, # optional key\n}, default_keys=Required)\n```\n\nThis way, keys marked with `Required()` will report errors if no value if provided.\n\nTypically, a marker \"decorates\" a mapping key, but some of them can be \"standalone\":\n\n```python\nfrom good import Schema, Extra\nSchema({\n 'name': str,\n Extra: int # allow any keys, provided their values are integer\n})\n```\n\nEach marker can have it's own unique behavior since nothing is hardcoded into the core [`Schema`](#schema).\nKeep on reading to learn how markers perform.\n\n\n## `Required`\n```python\nRequired(key)\n```\n\n`Required(key)` is used to decorate mapping keys and hence specify that these keys must always be present in\nthe input mapping.\n\nWhen compiled, [`Schema`](#schema) uses `default_keys` as the default marker:\n\n```python\nfrom good import Schema, Required\n\nschema = Schema({\n 'name': str,\n 'age': int\n}, default_keys=Required) # wrap with Required() by default\n\nschema({'name': 'Mark'})\n#-> Invalid: Required key not provided @ ['age']: expected age, got -none-\n```\n\nRemember that mapping keys are schemas as well, and `Require` will expect to always have a match:\n\n```python\nschema = Schema({\n Required(str): int,\n})\n\nschema({}) # no `str` keys provided\n#-> Invalid: Required key not provided: expected String, got -none-\n```\n\nIn addition, the `Required` marker has special behavior with [`Default`](#default) that allows to set the key\nto a default value if the key was not provided. More details in the docs for [`Default`](#default).\n\nArguments:\n\n\n\n\n\n\n\n## `Optional`\n```python\nOptional(key)\n```\n\n`Optional(key)` is controversial to [`Required(key)`](#required): specified that the mapping key is not required.\n\nThis only has meaning when a [`Schema`](#schema) has `default_keys=Required`:\nthen, it decorates all keys with `Required()`, unless a key is already decorated with some Marker.\n`Optional()` steps in: those keys are already decorated and hence are not wrapped with `Required()`.\n\nSo, it's only used to prevent `Schema` from putting `Required()` on a key.\nIn all other senses, it has absolutely no special behavior.\n\nAs a result, optional key can be missing, but if it was provided -- its value must match the value schema.\n\nExample: use as `default_keys`:\n\n```python\nschema = Schema({\n 'name': str,\n 'age': int\n}, default_keys=Optional) # Make all keys optional by default\n\nschema({}) #-> {} -- okay\nschema({'name': None})\n#-> Invalid: Wrong type @ ['name']: expected String, got None\n```\n\nExample: use to mark specific keys are not required:\n\n```python\nschema = Schema({\n 'name': str,\n Optional(str): int # key is optional\n})\n\nschema({'name': 'Mark'}) # valid\nschema({'name': 'Mark', 'age': 10}) # valid\nschema({'name': 'Mark', 'age': 'X'})\n#-> Invalid: Wrong type @ ['age']: expected Integer number, got Binary String\n```\n\nArguments:\n\n\n\n\n\n\n\n## `Remove`\n```python\nRemove(key)\n```\n\n`Remove(key)` marker is used to declare that the key, if encountered,\nshould be removed, without validating the value.\n\n`Remove` has highest priority, so it operates before everything else in the schema.\n\nExample:\n\n```python\nschema = Schema({\n Remove('name'): str, # `str` does not mean anything since the key is removed anyway\n 'age': int\n})\n\nschema({'name': 111, 'age': 18}) #-> {'age': 18}\n```\n\nHowever, it's more natural to use `Remove()` on values.\nRemember that in this case `'name'` will become [`Required()`](#required),\nif not decorated with [`Optional()`](#optional):\n\n```python\nschema = Schema({\n Optional('name'): Remove\n})\n\nschema({'name': 111, 'age': 18}) #-> {'age': 18}\n```\n\n**Bonus**: `Remove()` can be used in iterables as well:\n\n```python\nschema = Schema([str, Remove(int)])\nschema(['a', 'b', 1, 2]) #-> ['a', 'b']\n```\n\nArguments:\n\n\n\n\n\n\n\n## `Reject`\n```python\nReject(key)\n```\n\n`Reject(key)` marker is used to report [`Invalid`](#invalid) errors every time is matches something in the input.\n\nIt has lower priority than most of other schemas, so rejection will only happen\nif no other schemas has matched this value.\n\nExample:\n\n```python\nschema = Schema({\n Reject('name'): None, # Reject by key\n Optional('age'): Msg(Reject, u\"Field is not supported anymore\"), # alternative form\n})\n\nschema({'name': 111})\n#-> Invalid: Field is not supported anymore @ ['name']: expected -none-, got name\n```\n\nArguments:\n\n\n\n\n\n\n\n## `Allow`\n```python\nAllow(key)\n```\n\n`Allow(key)` is a no-op marker that never complains on anything.\n\nDesigned to be used with [`Extra`](#extra).\n\nArguments:\n\n\n\n\n\n\n\n## `Extra`\n```python\nExtra(key)\n```\n\n`Extra` is a catch-all marker to define the behavior for mapping keys not defined in the schema.\n\nIt has the lowest priority, and delegates its function to its value, which can be a schema, or another marker.\n\nGiven without argument, it's compiled with an identity function `lambda x:x` which is a catch-all:\nit matches any value. Together with lowest priority, `Extra` will only catch values which did not match anything else.\n\nEvery mapping has an `Extra` implicitly, and `extra_keys` argument controls the default behavior.\n\nExample with `Extra: `:\n\n```python\nschema = Schema({\n 'name': str,\n Extra: int # this will allow extra keys provided they're int\n})\n\nschema({'name': 'Alex', 'age': 18'}) #-> ok\nschema({'name': 'Alex', 'age': 'X'})\n#-> Invalid: Wrong type @ ['age']: expected Integer number, got Binary String\n```\n\nExample with `Extra: Reject`: reject all extra values:\n\n```python\nschema = Schema({\n 'name': str,\n Extra: Reject\n})\n\nschema({'name': 'Alex', 'age': 'X'})\n#-> Invalid: Extra keys not allowed @ ['age']: expected -none-, got age\n```\n\nExample with `Extra: Remove`: silently discard all extra values:\n\n```python\nschema = Schema({'name': str}, extra_keys=Remove)\nschema({'name': 'Alex', 'age': 'X'}) #-> {'name': 'Alex'}\n```\n\nExample with `Extra: Allow`: allow any extra values:\n\n```python\nschema = Schema({'name': str}, extra_keys=Allow)\nschema({'name': 'Alex', 'age': 'X'}) #-> {'name': 'Alex', 'age': 'X'}\n```\n\nArguments:\n\n\n\n\n\n\n\n## `Entire`\n```python\nEntire(key)\n```\n\n`Entire` is a convenience marker that validates the entire mapping using validators provided as a value.\n\nIt has absolutely lowest priority, lower than `Extra`, hence it never matches any keys, but is still executed to\nvalidate the mapping itself.\n\nThis opens the possibilities to define rules on multiple fields.\nThis feature is leveraged by the [`Inclusive`](#inclusive) and [`Exclusive`](#exclusive) group validators.\n\nFor example, let's require the mapping to have no more than 3 keys:\n\n```python\nfrom good import Schema, Entire\n\ndef maxkeys(n):\n # Return a validator function\n def validator(d):\n # `d` is the dictionary.\n # Validate it\n assert len(d) <= 3, 'Dict size should be <= 3'\n # Return the value since all callable schemas should do that\n return d\n return validator\n\nschema = Schema({\n str: int,\n Entire: maxkeys(3)\n})\n```\n\nIn this example, `Entire` is executed for every input dictionary, and magically calls the schema it's mapped to.\nThe `maxkeys(n)` schema is a validator that complains on the dictionary size if it's too huge.\n`Schema` catches the `AssertionError` thrown by it and converts it to [`Invalid`](#invalid).\n\nNote that the schema this marker is mapped to can't replace the mapping object, but it can mutate the given mapping.\n\nArguments:\n\n\n\n\n\n\n\n\nValidation Tools\n================\n\nAll validators listed here inherit from `ValidatorBase` which defines the standard interface.\nCurrently it makes no difference whether it's just a callable, a class, or a subclass of `ValidatorBase`,\nbut in the future it may gain special features.\n\nHelpers\n-------\nCollection of miscellaneous helpers to alter the validation process.\n\n\n### `Object`\n```python\nObject(schema, cls=None)\n```\n\nSpecify that the provided mapping should validate an object.\n\nThis uses the same mapping validation rules, but works with attributes instead:\n\n```python\nfrom good import Schema, Object\n\nintify = lambda v: int(v) # Naive Coerce(int) implementation\n\n# Define a class to play with\nclass Person:\n category = u'Something' # Not validated\n\n def __init__(self, name, age):\n self.name = name\n self.age = age\n\n# Schema\nschema = Schema(Object({\n 'name': str,\n 'age': intify,\n}))\n\n# Validate\nschema(Person(name=u'Alex', age='18')) #-> Girl(name=u'Alex', age=18)\n```\n\nInternally, it validates the object's `__dict__`: hence, class attributes are excluded from validation.\nValidation is performed with the help of a wrapper class which proxies object attributes as mapping keys,\nand then Schema validates it as a mapping.\n\nThis inherits the default required/extra keys behavior of the Schema.\nTo override, use [`Optional()`](#optional) and [`Extra`](#extra) markers.\n\nArguments:\n\n* `schema`: Object schema, given as a mapping\n* `cls`: Require instances of a specific class. If `None`, allows all classes.\n\n\n\n\n\n### `Msg`\n```python\nMsg(schema, message)\n```\n\nOverride the error message reported by the wrapped schema in case of validation errors.\n\nOn validation, if the schema throws [`Invalid`](#invalid) -- the message is overridden with `msg`.\n\nSome other error types are converted to `Invalid`: see notes on [Schema Callables](#callables).\n\n```python\nfrom good import Schema, Msg\n\nintify = lambda v: int(v) # Naive Coerce(int) implementation\nintify.name = u'Number'\n\nschema = Schema(Msg(intify, u'Need a number'))\nschema(1) #-> 1\nschema('a')\n#-> Invalid: Need a number: expected Number, got a\n```\n\nArguments:\n\n* `schema`: The wrapped schema to modify the error for\n* `message`: Error message to use instead of the one that's reported by the underlying schema\n\n\n\n\n\n### `Test`\n```python\nTest(fun)\n```\n\nTest the value with the provided function, expecting that it won't throw errors.\n\nIf no errors were thrown -- the value is valid and *the original input value is used*.\nIf any error was thrown -- the value is considered invalid.\n\nThis is especially useful to discard tranformations made by the wrapped validator:\n\n```python\nfrom good import Schema, Coerce\n\nschema = Schema(Coerce(int))\n\nschema(123) #-> 123\nschema('123') #-> '123' -- still string\nschema('abc')\n#-> Invalid: Invalid value, expected *Integer number, got abc\n```\n\nArguments:\n\n* `fun`: Callable to test the value with, or a validator function.\n\n Note that this won't work with mutable input values since they're modified in-place!\n\n\n\n\n\n### `message`\n```python\nmessage(message, name=None)\n```\n\nConvenience decorator that applies [`Msg()`](#msg) to a callable.\n\n```python\nfrom good import Schema, message\n\n@message(u'Need a number')\ndef intify(v):\n return int(v)\n```\n\nArguments:\n\n* `message`: Error message to use instead\n* `name`: Override schema name as well. See [`name`](#name).\n\nReturns: `callable` decorator\n\n\n\n### `name`\n```python\nname(name, validator=None)\n```\n\nSet a name on a validator callable.\n\nUseful for user-friendly reporting when using lambdas to populate the [`Invalid.expected`](#invalid) field:\n\n```python\nfrom good import Schema, name\n\nSchema(lambda x: int(x))('a')\n#-> Invalid: invalid literal for int(): expected (), got\nSchema(name('int()', lambda x: int(x))('a')\n#-> Invalid: invalid literal for int(): expected int(), got a\n```\n\nNote that it is only useful with lambdas, since function name is used if available:\nsee notes on [Schema Callables](#callables).\n\nArguments:\n\n* `name`: Name to assign on the validator callable\n* `validator`: Validator callable. If not provided -- a decorator is returned instead:\n\n ```python\n from good import name\n\n @name(u'int()')\n def int(v):\n return int(v)\n ```\n\nReturns: `callable` The same validator callable\n\n\n\n### `truth`\n```python\ntruth(message, expected=None)\n```\n\nConvenience decorator that applies [`Check`](#check) to a callable.\n\n```python\nfrom good import truth\n\n@truth(u'Must be an existing directory')\ndef isDir(v):\n return os.path.isdir(v)\n```\n\nArguments:\n\n* `message`: Validation error message\n* `expected`: Expected value string representation, or `None` to get it from the wrapped callable\n\nReturns: `callable` decorator\n\n\n\n\nPredicates\n----------\n\n\n\n### `Maybe`\n```python\nMaybe(schema, none=None)\n```\n\nValidate the the value either matches the given schema or is None.\n\nThis supports *nullable* values and gives them a good representation.\n\n```python\nfrom good import Schema, Maybe, Email\n\nschema = Schema(Maybe(Email))\n\nschema(None) #-> None\nschema('user@example.com') #-> 'user@example.com'\nscheam('blahblah')\n#-> Invalid: Wrong E-Mail: expected E-Mail?, got blahblah\n```\n\nNote that it also have the [`Default`-like behavior](#default)\nthat initializes the missing [`Required()`](#required) keys:\n\n```python\nschema = Schema({\n 'email': Maybe(Email)\n})\n\nschema({}) #-> {'email': None}\n```\n\nArguments:\n\n* `schema`: Schema for a provided value\n* `none`: Empty value literal\n\n\n\n\n\n### `Any`\n```python\nAny(*schemas)\n```\n\nTry the provided schemas in order and use the first one that succeeds.\n\nThis is the *OR* condition predicate: any of the schemas should match.\n[`Invalid`](#invalid) error is reported if neither of the schemas has matched.\n\n```python\nfrom good import Schema, Any\n\nschema = Schema(Any(\n # allowed string constants\n 'true', 'false',\n # otherwise coerce as a bool\n lambda v: 'true' if v else 'false'\n))\nschema('true') #-> 'true'\nschema(0) #-> 'false'\n```\n\nArguments:\n\n* `*schemas`: List of schemas to try.\n\n\n\n\n\n### `All`\n```python\nAll(*schemas)\n```\n\nValue must pass all validators wrapped with `All()` predicate.\n\nThis is the *AND* condition predicate: all of the schemas should match in order,\nwhich is in fact a composition of validators: `All(f,g)(value) = g(f(value))`.\n\n```python\nfrom good import Schema, All, Range\n\nschema = Schema(All(\n # Must be an integer ..\n int,\n # .. and in the allowed range\n Range(0, 10)\n))\n\nschema(1) #-> 1\nschema(99)\n#-> Invalid: Not in range: expected 0..10, got 99\n```\n\nArguments:\n\n* `*schemas`: List of schemas to apply.\n\n\n\n\n\n### `Neither`\n```python\nNeither(*schemas)\n```\n\nValue must not match any of the schemas.\n\nThis is the *NOT* condition predicate: a value is considered valid if each schema has raised an error.\n\n```python\nfrom good import Schema, All, Neither\n\nschema = Schema(All(\n # Integer\n int,\n # But not zero\n Neither(0)\n))\n\nschema(1) #-> 1\nschema(0)\n#-> Invalid: Value not allowed: expected Not(0), got 0\n```\n\nArguments:\n\n* `*schemas`: List of schemas to check against.\n\n\n\n\n\n### `Inclusive`\n```python\nInclusive(*keys)\n```\n\n`Inclusive` validates the defined inclusive group of mapping keys:\nif any of them was provided -- then all of them become required.\n\nThis exists to support \"sub-structures\" within the mapping which only make sense if specified together.\nSince this validator works on the entire mapping, the best way is to use it together with the [`Entire`](#entire)\nmarker:\n\n```python\nfrom good import Schema, Entire, Inclusive\n\nschema = Schema({\n # Fields for all files\n 'name': str,\n # Fields for images only\n Optional('width'): int,\n Optional('height'): int,\n # Now put a validator on the entire mapping\n Entire: Inclusive('width', 'height')\n})\n\nschema({'name': 'monica.jpg'}) #-> ok\nschema({'name': 'monica.jpg', 'width': 800, 'height': 600}) #-> ok\nschema({'name': 'monica.jpg', 'width': 800})\n#-> Invalid: Required key not provided: expected height, got -none-\n```\n\nNote that `Inclusive` only supports literals.\n\nArguments:\n\n* `*keys`: List of mutually inclusive keys (literals).\n\n\n\n\n\n### `Exclusive`\n```python\nExclusive(*keys)\n```\n\n`Exclusive` validates the defined exclusive group of mapping keys:\nif any of them was provided -- then none of the remaining keys can be used.\n\nThis supports \"sub-structures\" with choice: if the user chooses a field from one of them --\nthen he cannot use others.\nIt works on the entire mapping and hence best to use with the [`Entire`](#entire) marker.\n\nBy default, `Exclusive` requires the user to choose one of the options,\nbut this can be overridden with [`Optional`](#optional) marker class given as an argument:\n\n```python\nfrom good import Exclusive, Required, Optional\n\n# Requires either of them\nExclusive('login', 'password')\nExclusive(Required, 'login', 'password') # the default\n\n# Requires either of them, or none\nExclusive(Optional, 'login', 'password')\n```\n\nLet's demonstrate with the API that supports multiple types of authentication,\nbut requires the user to choose just one:\n\n```python\nfrom good import Schema, Entire, Exclusive\n\nschema = Schema({\n # Authentication types: login+password | email+password\n Optional('login'): str,\n Optional('email'): str,\n 'password': str,\n # Now put a validator on the entire mapping\n # that forces the user to choose\n Entire: Msg( # also override the message\n Exclusive('login', 'email'),\n u'Choose one'\n )\n})\n\nschema({'login': 'kolypto', 'password': 'qwerty'}) #-> ok\nschema({'email': 'kolypto', 'password': 'qwerty'}) #-> ok\nschema({'login': 'a', 'email': 'b', 'password': 'c'})\n#-> MultipleInvalid:\n#-> Invalid: Choose one @ [login]: expected login|email, got login\n#-> Invalid: Choose one @ [email]: expected login|email, got email\n```\n\nNote that `Exclusive` only supports literals.\n\nArguments:\n\n* `*keys`: List of mutually exclusive keys (literals).\n\n Can contain [`Required`](#required) or [`Optional`](#optional) marker classes,\n which defines the behavior when no keys are provided. Default is `Required`.\n\n\n\n\n\n\nTypes\n-----\n\n\n\n### `Type`\n```python\nType(*types)\n```\n\nCheck if the value has the specific type with `isinstance()` check.\n\nIn contrast to [Schema types](#schema) which performs a strict check, this check is relaxed and accepts subtypes\nas well.\n\n```python\nfrom good import Schema, Type\n\nschema = Schema(Type(int))\nschema(1) #-> 1\nschema(True) #-> True\n```\n\nArguments:\n\n* `*types`: The type to check instances against.\n\n If multiple types are provided, then any of them is acceptable.\n\n\n\n\n\n### `Coerce`\n```python\nCoerce(constructor)\n```\n\nCoerce a value to a type with the provided callable.\n\n`Coerce` applies the *constructor* to the input value and returns a value cast to the provided type.\n\nIf *constructor* fails with `TypeError` or `ValueError`, the value is considered invalid and `Coerce` complains\non that with a custom message.\n\nHowever, if *constructor* raises [`Invalid`](#invalid) -- the error object is used as it.\n\n```python\nfrom good import Schema, Coerce\n\nschema = Schema(Coerce(int))\nschema(u'1') #-> 1\nschema(u'a')\n#-> Invalid: Invalid value: expected *Integer number, got a\n```\n\nArguments:\n\n* `constructor`: Callable that typecasts the input value\n\n\n\n\n\n\nValues\n------\n\n\n\n### `In`\n```python\nIn(container)\n```\n\nValidate that a value is in a collection.\n\nThis is a plain simple `value in container` check, where `container` is a collection of literals.\n\nIn contrast to [`Any`](#any), it does not compile its arguments into schemas,\nand hence achieves better performance.\n\n```python\nfrom good import Schema, In\n\nschema = Schema(In({1, 2, 3}))\n\nschema(1) #-> 1\nschema(99)\n#-> Invalid: Unsupported value: expected In(1,2,3), got 99\n```\n\nThe same example will work with [`Any`](#any), but slower :-)\n\nArguments:\n\n* `container`: Collection of allowed values.\n\n In addition to naive tuple/list/set/dict, this can be any object that supports `in` operation.\n\n\n\n\n\n### `Length`\n```python\nLength(min=None, max=None)\n```\n\nValidate that the provided collection has length in a certain range.\n\n```python\nfrom good import Schema, Length\n\nschema = Schema(All(\n # Ensure it's a list (and not any other iterable type)\n list,\n # Validate length\n Length(max=3),\n))\n```\n\nSince mappings also have length, they can be validated as well:\n\n```python\nschema = Schema({\n # Strings mapped to integers\n str: int,\n # Size = 1..3\n # Empty dicts are not allowed since `str` is implicitly `Required(str)`\n Entire: Length(max=3)\n})\n\nschema([1]) #-> ok\nschema([1,2,3,4])\n#-> Invalid: Too long (3 is the most): expected Length(..3), got 4\n```\n\nArguments:\n\n* `min`: Minimal allowed length, or `None` to impose no limits.\n* `max`: Maximal allowed length, or `None` to impose no limits.\n\n\n\n\n\n### `Default`\n```python\nDefault(default)\n```\n\nInitialize a value to a default if it's not provided.\n\n\"Not provided\" means `None`, so basically it replaces `None`s with the default:\n\n```python\nfrom good import Schema, Any, Default\n\nschema = Schema(Any(\n # Accept ints\n int,\n # Replace `None` with 0\n Default(0)\n))\n\nschema(1) #-> 1\nschema(None) #-> 0\n```\n\nIt raises [`Invalid`](#invalid) on all values except for `None` and `default`:\n\n```python\nschema = Schema(Default(42))\n\nschema(42) #-> 42\nschema(None) #-> 42\nschema(1)\n#-> Invalid: Invalid value\n```\n\nIn addition, `Default` has special behavior with `Required` marker which is built into it:\nif a required key was not provided -- it's created with the default value:\n\n```python\nfrom good import Schema, Default\n\nschema = Schema({\n # remember that keys are implicitly required\n 'name': str,\n 'age': Any(int, Default(0))\n})\n\nschema({'name': 'Alex'}) #-> {'name': 'Alex', 'age': 0}\n```\n\nArguments:\n\n* `default`: The default value to use\n\n\n\n\n\n### `Fallback`\n```python\nFallback(default)\n```\n\nAlways returns the default value.\n\nWorks like [`Default`](#default), but does not fail on any values.\n\nTypical usage is to terminate [`Any`](#any) chain in case nothing worked:\n\n```python\nfrom good import Schema, Any, Fallback\n\nschema = Schema(Any(\n int,\n # All non-integer numbers are replaced with `None`\n Fallback(None)\n))\n```\n\nLike [`Default`](#default), it also works with mappings.\n\nInternally, `Default` and `Fallback` work by feeding the schema with a special [`Undefined`](good/schema/util.py) value:\nif the schema manages to return some value without errors -- then it has the named \"default behavior\",\nand this validator just leverages the feature.\n\nA \"fallback value\" may be provided manually, and will work absolutely the same\n(since value schema manages to succeed even though `Undefined` was given):\n\n```python\nschema = Schema({\n 'name': str,\n 'age': Any(int, lambda v: 42)\n})\n```\n\nArguments:\n\n* `default`: The value that's always returned\n\n\n\n\n\n### `Map`\n```python\nMap(enum, mode=1)\n```\n\nConvert Enumerations that map names to values.\n\nSupports three kinds of enumerations:\n\n1. Mapping.\n\n Provided a mapping from names to values,\n converts the input to values by mapping key:\n\n ```python\n from good import Schema, Map\n schema = Schema(Map({\n 'RED': 0xFF0000,\n 'GREEN': 0x00FF00,\n 'BLUE': 0x0000FF\n }))\n\n schema('RED') #-> 0xFF0000\n schema('BLACK')\n #-> Invalid: Unsupported value: expected Constant, provided BLACK\n ```\n\n2. Class.\n\n Provided a class with attributes (names) initialized with values,\n converts the input to values matching by attribute name:\n\n ```python\n class Colors:\n RED = 0xFF0000\n GREEN = 0x00FF00\n BLUE = 0x0000FF\n\n schema = Schema(Map(Colors))\n\n schema('RED') #-> 0xFF0000\n schema('BLACK')\n #-> Invalid: Unsupported value: expected Colors, provided BLACK\n ```\n\n Note that all attributes of the class are used, except for protected (`_name`) and callables.\n\n3. Enum.\n\n Supports [Python 3.4 Enums](https://docs.python.org/3/library/enum.html)\n and the backported [enum34](https://pypi.python.org/pypi/enum34).\n\n Provided an enumeration, converts the input to values by name.\n In addition, enumeration value can pass through safely:\n\n ```python\n from enum import Enum\n\n class Colors(Enum):\n RED = 0xFF0000\n GREEN = 0x00FF00\n BLUE = 0x0000FF\n\n schema = Schema(Map(Colors))\n schema('RED') #-> \n schema('BLACK')\n #-> Invalid: Unsupported value: expected Colors, provided BLACK\n ```\n\n Note that in `mode=Map.VAL` it works precisely like `Schema(Enum)`.\n\nIn addition to the \"straignt\" mode (lookup by key), it supports reverse matching:\n\n* When `mode=Map.KEY`, does only forward matching (by key) -- the default\n* When `mode=Map.VAL`, does only reverse matching (by value)\n* When `mode=Map.BOTH`, does bidirectional matching (by key first, then by value)\n\nAnother neat feature is that `Map` supports `in` containment checks,\nwhich works great together with [`In`](#in): `In(Map(enum-value))` will test if a value is convertible, but won't\nactually do the convertion.\n\n```python\nfrom good import Schema, Map, In\n\nschema = Schema(In(Map(Colors)))\n\nschema('RED') #-> 'RED'\nschema('BLACK')\n#-> Invalid: Unsupported value, expected Colors, got BLACK\n```\n\nArguments:\n\n* `enum`: Enumeration: dict, object, of Enum\n* `mode`: Matching mode: one of Map.KEY, Map.VAL, Map.BOTH\n\n\n\n\n\n\nBoolean\n-------\n\n\n\n### `Check`\n```python\nCheck(bvalidator, message, expected)\n```\n\nUse the provided boolean function as a validator and raise errors when it's `False`.\n\n```python\nimport os.path\nfrom good import Schema, Check\n\nschema = Schema(\n Check(os.path.isdir, u'Must be an existing directory'))\nschema('/') #-> '/'\nschema('/404')\n#-> Invalid: Must be an existing directory: expected isDir(), got /404\n```\n\nArguments:\n\n* `bvalidator`: Boolean validator function\n* `message`: Error message to report when `False`\n* `expected`: Expected value string representation, or `None` to get it from the wrapped callable\n\n\n\n\n\n### `Truthy`\n```python\nTruthy()\n```\n\nAssert that the value is truthy, in the Python sense.\n\nThis fails on all \"falsy\" values: `False`, `0`, empty collections, etc.\n\n```python\nfrom good import Schema, Truthy\n\nschema = Schema(Truthy())\n\nschema(1) #-> 1\nschema([1,2,3]) #-> [1,2,3]\nschema(None)\n#-> Invalid: Empty value: expected truthy(), got None\n```\n\n\n\n\n\n\n### `Falsy`\n```python\nFalsy()\n```\n\nAssert that the value is falsy, in the Python sense.\n\nSupplementary to [`Truthy`](#truthy).\n\n\n\n\n\n\n### `Boolean`\n```python\nBoolean()\n```\n\nConvert human-readable boolean values to a `bool`.\n\nThe following values are supported:\n\n* `None`: `False`\n* `bool`: direct\n* `int`: `0` = `False`, everything else is `True`\n* `str`: Textual boolean values, compatible with [YAML 1.1 boolean literals](http://yaml.org/type/bool.html), namely:\n\n y|Y|yes|Yes|YES|n|N|no|No|NO|\n true|True|TRUE|false|False|FALSE|\n on|On|ON|off|Off|OFF\n\n [`Invalid`](#invalid) is thrown if an unknown string literal is provided.\n\nExample:\n\n```python\nfrom good import Schema, Boolean\n\nschema = Schema(Boolean())\n\nschema(None) #-> False\nschema(0) #-> False\nschema(1) #-> True\nschema(True) #-> True\nschema(u'yes') #-> True\n```\n\n\n\n\n\n\n\nNumbers\n-------\n\n\n\n### `Range`\n```python\nRange(min=None, max=None)\n```\n\nValidate that the value is within the defined range, inclusive.\nRaise [`Invalid`](#invalid) error if not.\n\n```python\nfrom good import Schema, Range\n\nschema = Schema(Range(1, 10))\n\nschema(1) #-> 1\nschema(10) #-> 10\nschema(15)\n#-> Invalid: Value must be at most 10: expected Range(1..10), got 15\n```\n\nIf the value cannot be compared to a number -- raises [`Invalid`](#invalid).\nNote that in Python2 almost everything can be compared to a number, including strings, dicts and lists!\n\nArguments:\n\n* `min`: Minimal allowed value, or `None` to impose no limits.\n* `max`: Maximal allowed value, or `None` to impose no limits.\n\n\n\n\n\n### `Clamp`\n```python\nClamp(min=None, max=None)\n```\n\nClamp a value to the defined range, inclusive.\n\n```python\nfrom good import Schema, Clamp\n\nschema = Schema(Clamp(1, 10))\n\nschema(-1) #-> 1\nschema(1) #-> 1\nschema(10) #-> 10\nschema(15) #-> 10\n```\n\nIf the value cannot be compared to a number -- raises [`Invalid`](#invalid).\nNote that in Python2 almost everything can be compared to a number, including strings, dicts and lists!\n\nArguments:\n\n* `min`: Minimal allowed value, or `None` to impose no limits.\n* `max`: Maximal allowed value, or `None` to impose no limits.\n\n\n\n\n\n\nStrings\n-------\n\n\n\n### `Lower`\n```python\nLower()\n```\n\nCasts the provided string to lowercase, fails is the input value is not a string.\n\nSupports both binary and unicode strings.\n\n```python\nfrom good import Schema, Lower\n\nschema = Schema(Lower())\n\nschema(u'ABC') #-> u'abc'\nschema(123)\n#-> Invalid: Not a string: expected String, provided Integer number\n```\n\n\n\n\n\n\n### `Upper`\n```python\nUpper()\n```\n\nCasts the input string to UPPERCASE.\n\n\n\n\n\n\n### `Capitalize`\n```python\nCapitalize()\n```\n\nCapitalizes the input string.\n\n\n\n\n\n\n### `Title`\n```python\nTitle()\n```\n\nCasts The Input String To Title Case\n\n\n\n\n\n\n### `Match`\n```python\nMatch(pattern, message=None, expected=None)\n```\n\nValidate the input string against a regular expression.\n\n```python\nfrom good import Schema, Match\n\nschema = Schema(All(\n unicode,\n Match(r'^0x[A-F0-9]+$', 'hex number')\n))\n\nschema('0xDEADBEEF') #-> '0xDEADBEEF'\nschema('0x')\n#-> Invalid: Wrong format: expected hex number, got 0xDEADBEEF\n```\n\nArguments:\n\n* `pattern`: RegExp pattern to match with: a string, or a compiled pattern\n* `message`: Error message override\n* `expected`: Textual representation of what's expected from the user\n\n\n\n\n\n### `Replace`\n```python\nReplace(pattern, repl, message=None, expected=None)\n```\n\nRegExp substitution.\n\n```python\nfrom good import Schema, Replace\n\nschema = Schema(Replace(\n # Grab domain name\n r'^https?://([^/]+)/.*'\n # Replace\n r'\u0001',\n # Tell the user that we're expecting a URL\n u'URL'\n))\n\nschema('http://example.com/a/b/c') #-> 'example.com'\nschema('user@example.com')\n#-> Invalid: Wrong format: expected URL, got user@example.com\n```\n\nArguments:\n\n* `pattern`: RegExp pattern to match with: a string, or a compiled pattern\n* `repl`: Replacement pattern.\n\n Backreferences are supported, just like in the [`re`](https://docs.python.org/2/library/re.html) module.\n* `message`: Error message override\n* `expected`: Textual representation of what's expected from the user\n\n\n\n\n\n### `Url`\n```python\nUrl(protocols=('http', 'https'))\n```\n\nValidate a URL, make sure it's in the absolute format, including the protocol.\n\n```python\nfrom good import Schema, Url\n\nschema = Schema(Url('https'))\n\nschema('example.com') #-> 'https://example.com'\nschema('http://example.com') #-> 'http://example.com'\n```\n\nArguments:\n\n* `protocols`: List of allowed protocols.\n\n If no protocol is provided by the user -- the first protocol is used by default.\n\n\n\n\n\n### `Email`\n```python\nEmail()\n```\n\nValidate that a value is an e-mail address.\n\nThis simply tests for the presence of the '@' sign, surrounded by some characters.\n\n```python\nfrom good import Email\n\nschema = Schema(Email())\n\nschema('user@example.com') #-> 'user@example.com'\nschema('user@localhost') #-> 'user@localhost'\nschema('user')\n#-> Invalid: Invalid e-mail: expected E-Mail, got user\n```\n\n\n\n\n\n\n\nDates\n-----\n\n\n\n### `DateTime`\n```python\nDateTime(formats, localize=None, astz=None)\n```\n\nValidate that the input is a Python `datetime`.\n\nSupports the following input values:\n\n1. `datetime`: passthrough\n2. string: parses the string with any of the specified formats\n (see [strptime()](https://docs.python.org/3.4/library/datetime.html#strftime-and-strptime-behavior))\n\n```python\nfrom datetime import datetime\nfrom good import Schema, DateTime\n\nschema = Schema(DateTime('%Y-%m-%d %H:%M:%S'))\n\nschema('2014-09-06 21:22:23') #-> datetime.datetime(2014, 9, 6, 21, 22, 23)\nschema(datetime.now()) #-> datetime.datetime(2014, 9, 6, 21, 22, 23)\nschema('2014')\n#-> Invalid: Invalid datetime format, expected DateTime, got 2014.\n```\n\nNotes on timezones:\n\n* If the format does not support timezones, it always returns *naive* `datetime` objects (without `tzinfo`).\n* If timezones are supported by the format (with `%z`/`%Z`),\n it returns an *aware* `datetime` objects (with `tzinfo`).\n* Since Python2 does not always support `%z` -- `DateTime` does this manually.\n Due to the limited nature of this workaround, the support for `%z` only works if it's at the end of the string!\n\nAs a result, '00:00:00' is parsed into a *naive* datetime, and '00:00:00 +0200' results in an *aware* datetime.\n\nIf your application wants different rules, use `localize` and `astz`:\n\n* `localize` argument is the default timezone to set on *naive* datetimes,\n or a callable which is applied to the input and should return adjusted `datetime`.\n* `astz` argument is the timezone to adjust the *aware* datetime to, or a callable.\n\nThen the generic recipe is:\n\n* Set `localize` to the timezone (or a callable) that you expect the user to input the datetime in\n* Set `astz` to the timezone you wish to have in the result.\n\nThis works best with the excellent [pytz](http://pytz.sourceforge.net/) library:\n\n```python\nimport pytz\nfrom good import Schema, DateTime\n\n# Formats: with and without timezone\nformats = [\n '%Y-%m-%d %H:%M:%S',\n '%Y-%m-%d %H:%M:%S%z'\n]\n\n# The used timezones\nUTC = pytz.timezone('UTC')\nOslo = pytz.timezone('Europe/Oslo')\n\n### Example: Use Europe/Oslo by default\nschema = Schema(DateTime(\n formats,\n localize=Oslo\n))\n\nschema('2014-01-01 00:00:00')\n#-> datetime.datetime(2014, 1, 1, 0, 0, tzinfo='Europe/Oslo')\nschema('2014-01-01 00:00:00-0100')\n#-> datetime.datetime(2014, 1, 1, 0, 0, tzinfo=-0100)\n\n### Example: Use Europe/Oslo by default and convert to an aware UTC\nschema = Schema(DateTime(\n formats,\n localize=Oslo,\n astz=UTC\n))\n\nschema('2014-01-01 00:00:00')\n#-> datetime.datetime(2013, 12, 31, 23, 17, tzinfo=)\nschema('2014-01-01 00:00:00-0100')\n#-> datetime.datetime(2014, 1, 1, 1, 0, tzinfo=)\n\n### Example: Use Europe/Oslo by default, convert to a naive UTC\n# This is the recommended way\nschema = Schema(DateTime(\n formats,\n localize=Oslo,\n astz=lambda v: v.astimezone(UTC).replace(tzinfo=None)\n))\n\nschema('2014-01-01 00:00:00')\n#-> datetime.datetime(2013, 12, 31, 23, 17)\nschema('2014-01-01 00:00:00-0100')\n#-> datetime.datetime(2014, 1, 1, 1, 0)\n```\n\nNote: to save some pain, make sure to *always* work with naive `datetimes` adjusted to UTC!\nArmin Ronacher [explains it here](http://lucumr.pocoo.org/2011/7/15/eppur-si-muove/).\n\nSummarizing all the above, the validation procedure is a 3-step process:\n\n1. Parse (only with strings)\n2. If is *naive* -- apply `localize` and make it *aware* (if `localize` is specified)\n3. If is *aware* -- apply `astz` to convert it (if `astz` is specified)\n\nArguments:\n\n* `formats`: Supported format string, or an iterable of formats to try them all.\n* `localize`: Adjust *naive* `datetimes` to a timezone, making it *aware*.\n\n A `tzinfo` timezone object,\n or a callable which is applied to a *naive* datetime and should return an adjusted value.\n\n Only called for *naive* `datetime`s.\n* `astz`: Adjust *aware* `datetimes` to another timezone.\n\n A `tzinfo` timezone object,\n or a callable which is applied to an *aware* datetime and should return an adjusted value.\n\n Only called for *aware* `datetime`s, including those created by `localize`\n\n\n\n\n\n### `Date`\n```python\nDate(formats, localize=None, astz=None)\n```\n\nValidate that the input is a Python `date`.\n\nSupports the following input values:\n\n1. `date`: passthrough\n2. `datetime`: takes the `.date()` part\n2. string: parses (see [`DateTime`](#datetime))\n\n```python\nfrom datetime import date\nfrom good import Schema, Date\n\nschema = Schema(Date('%Y-%m-%d'))\n\nschema('2014-09-06') #-> datetime.date(2014, 9, 6)\nschema(date(2014, 9, 6)) #-> datetime.date(2014, 9, 6)\nschema('2014')\n#-> Invalid: Invalid date format, expected Date, got 2014.\n```\n\nArguments:\n\n\n\n\n\n\n\n\n\n### `Time`\n```python\nTime(formats, localize=None, astz=None)\n```\n\nValidate that the input is a Python `time`.\n\nSupports the following input values:\n\n1. `time`: passthrough\n2. `datetime`: takes the `.timetz()` part\n2. string: parses (see [`DateTime`](#datetime))\n\nSince `time` is subject to timezone problems,\nmake sure you've read the notes in the relevant section of [`DateTime`](#datetime) docs.\n\nArguments:\n\n\n\n\n\n\n\n\n\n\nFiles\n-----\n\n\n\n### `IsFile`\n```python\nIsFile()\n```\n\nVerify that the file exists.\n\n```python\nfrom good import Schema, IsFile\n\nschema = Schema(IsFile())\n\nschema('/etc/hosts') #-> '/etc/hosts'\nschema('/etc')\n#-> Invalid: is not a file: expected Existing file path, got /etc\n```\n\n\n\n\n\n\n### `IsDir`\n```python\nIsDir()\n```\n\nVerify that the directory exists.\n\n\n\n\n\n\n### `PathExists`\n```python\nPathExists()\n```\n\nVerify that the path exists.\n\n\n\n\n\n\n\n\n", "description_content_type": "text/markdown", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/kolypto/py-good", "keywords": "validation", "license": "BSD", "maintainer": "", "maintainer_email": "", "name": "good", "package_url": "https://pypi.org/project/good/", "platform": "any", "project_url": "https://pypi.org/project/good/", "project_urls": { "Homepage": "https://github.com/kolypto/py-good" }, "release_url": "https://pypi.org/project/good/0.0.8/", "requires_dist": null, "requires_python": "", "summary": "Slim yet handsome validation library", "version": "0.0.8" }, "last_serial": 5990012, "releases": { "0.0.1-0": [ { "comment_text": "", "digests": { "md5": "90d53cad9d2d7ddaa77f5b859d0517bc", "sha256": "a000f4f454ed451fa03f0b29f29e27b15380fa39389c74630d02cc7a782d1995" }, "downloads": -1, "filename": "good-0.0.1_0-py2-none-any.whl", "has_sig": false, "md5_digest": "90d53cad9d2d7ddaa77f5b859d0517bc", "packagetype": "bdist_wheel", "python_version": "2.7", "requires_python": null, "size": 68353, "upload_time": "2014-09-04T23:46:06", "url": "https://files.pythonhosted.org/packages/79/75/1525cbe27f8a94663b5a56f8508f4e57790ebc13b13011c3a0c5b1bf492f/good-0.0.1_0-py2-none-any.whl" }, { "comment_text": "", "digests": { "md5": "9b232ed4462707cfa2d4ce71381fe296", "sha256": "4acd36ba5385776576dd12cebdea2292cf7b6a0a3c940c8352cad4dac1ce366d" }, "downloads": -1, "filename": "good-0.0.1-0.tar.gz", "has_sig": false, "md5_digest": "9b232ed4462707cfa2d4ce71381fe296", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 110170, "upload_time": "2014-09-04T23:46:03", "url": "https://files.pythonhosted.org/packages/b7/80/53bd5e6ff3b285981dc4561cb13afee08634dc42c072885ddf4defb76521/good-0.0.1-0.tar.gz" } ], "0.0.1-1": [ { "comment_text": "", "digests": { "md5": "45519561f028b770a749f0045a7ca4d9", "sha256": "65875cfb168bee4c2022bb05b81e5185b3f474f3b62e4c8271a2157fa69dead9" }, "downloads": -1, "filename": "good-0.0.1_1-py2-none-any.whl", "has_sig": false, "md5_digest": "45519561f028b770a749f0045a7ca4d9", "packagetype": "bdist_wheel", "python_version": "2.7", "requires_python": null, "size": 68349, "upload_time": "2014-09-04T23:46:43", "url": "https://files.pythonhosted.org/packages/a8/12/202fccf6424406fd341e6f30924bbe63eaa461a58cc776d3e0df26a9e0d8/good-0.0.1_1-py2-none-any.whl" }, { "comment_text": "", "digests": { "md5": "7d9ff4c67e40d4e237747942a670c0c4", "sha256": "03dbfd5e8a12287103915b73be9ebf281e3ff3936b94ac2cd7724e8eec2a183e" }, "downloads": -1, "filename": "good-0.0.1-1.tar.gz", "has_sig": false, "md5_digest": "7d9ff4c67e40d4e237747942a670c0c4", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 110153, "upload_time": "2014-09-04T23:46:40", "url": "https://files.pythonhosted.org/packages/5c/3c/03fbebb9b1caf2b1c9534ac42691f05271bc762849e3791b1489739f8ed9/good-0.0.1-1.tar.gz" } ], "0.0.2-0": [ { "comment_text": "", "digests": { "md5": "7acc81fade519021c42a3bc0a5207501", "sha256": "3586887738d9f5f4ba2961ac077431edf5cb35efab7879cceedf7dd17581889b" }, "downloads": -1, "filename": "good-0.0.2_0-py2-none-any.whl", "has_sig": false, "md5_digest": "7acc81fade519021c42a3bc0a5207501", "packagetype": "bdist_wheel", "python_version": "2.7", "requires_python": null, "size": 69159, "upload_time": "2014-09-05T13:02:27", "url": "https://files.pythonhosted.org/packages/e1/87/db8be639b10576efff0cf738b7a6c5a8c3d7b5b080895837c4a354778658/good-0.0.2_0-py2-none-any.whl" }, { "comment_text": "", "digests": { "md5": "0412ea079a70d39b835d479964626497", "sha256": "986c4fcd45e006cc2d9308e41fc3b4c0775ada799c52b0a6e711cd32a520e7af" }, "downloads": -1, "filename": "good-0.0.2-0.tar.gz", "has_sig": false, "md5_digest": "0412ea079a70d39b835d479964626497", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 129648, "upload_time": "2014-09-05T13:02:24", "url": "https://files.pythonhosted.org/packages/b5/02/04f78d50a6c750ab6ab352fbbccb9c2756e7a975cbf5fce070ebc17bdad4/good-0.0.2-0.tar.gz" } ], "0.0.2-1": [ { "comment_text": "", "digests": { "md5": "03676e94f1c80b49f34a4b07a791b3c7", "sha256": "c5ab8ccba22e4125fc47d6775a9264fb988dfff03e936616aeb2f478c2ca79de" }, "downloads": -1, "filename": "good-0.0.2_1-py2-none-any.whl", "has_sig": false, "md5_digest": "03676e94f1c80b49f34a4b07a791b3c7", "packagetype": "bdist_wheel", "python_version": "2.7", "requires_python": null, "size": 69159, "upload_time": "2014-09-05T13:10:32", "url": "https://files.pythonhosted.org/packages/31/04/82e4980d8eaa5aaf50c674a79565a7c5291be22ee418a269c73e0f5f57d8/good-0.0.2_1-py2-none-any.whl" }, { "comment_text": "", "digests": { "md5": "41c55b2b6b791246abc95fe7344bcdb8", "sha256": "24493279ad7c8a4967e554ec7458c80b0584e8b30d7d43cb7287ae29fba24ba3" }, "downloads": -1, "filename": "good-0.0.2-1.tar.gz", "has_sig": false, "md5_digest": "41c55b2b6b791246abc95fe7344bcdb8", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 129650, "upload_time": "2014-09-05T13:10:28", "url": "https://files.pythonhosted.org/packages/5e/a5/8cf22f82343df1b5b2995591af4b6d219eacf34cc231bac991190e87fad5/good-0.0.2-1.tar.gz" } ], "0.0.3-0": [ { "comment_text": "", "digests": { "md5": "7dffc32afe3a028307dd69ef847b440e", "sha256": "f5b4599f4cab7f4a4a2755d2336a1744b24cbbf5cbb25c47699e4cb96755d3c4" }, "downloads": -1, "filename": "good-0.0.3_0-py2-none-any.whl", "has_sig": false, "md5_digest": "7dffc32afe3a028307dd69ef847b440e", "packagetype": "bdist_wheel", "python_version": "2.7", "requires_python": null, "size": 70070, "upload_time": "2014-09-05T14:56:46", "url": "https://files.pythonhosted.org/packages/7d/c2/e5d635fa1cdee862b4528ad3d498b68d9d5baee4d4e0c98d7571cb2db213/good-0.0.3_0-py2-none-any.whl" }, { "comment_text": "", "digests": { "md5": "1c36563c832568bde7e01faeb6f570ba", "sha256": "38a805cddfde9f40153c817561a9f129bfabd4ad68ba4831c03892e4936def04" }, "downloads": -1, "filename": "good-0.0.3-0.tar.gz", "has_sig": false, "md5_digest": "1c36563c832568bde7e01faeb6f570ba", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 130791, "upload_time": "2014-09-05T14:56:41", "url": "https://files.pythonhosted.org/packages/cf/63/043a9552ce458eb1aa4916e210b06f77c93055efe1a17bca3cf8a0da9f47/good-0.0.3-0.tar.gz" } ], "0.0.3-1": [ { "comment_text": "", "digests": { "md5": "59b762175775fbbb536d2d4256c5f9dd", "sha256": "28cc218caa3d7c58cea8d056d8c3c827538643a8bf681e2c1038fd9d059ba1ab" }, "downloads": -1, "filename": "good-0.0.3_1-py34-none-any.whl", "has_sig": false, "md5_digest": "59b762175775fbbb536d2d4256c5f9dd", "packagetype": "bdist_wheel", "python_version": "2.7", "requires_python": null, "size": 70073, "upload_time": "2014-09-05T15:07:54", "url": "https://files.pythonhosted.org/packages/cd/9d/ac61365c4aaca65406c6084fff140846e57c99f662fcbc36da5b451068bd/good-0.0.3_1-py34-none-any.whl" }, { "comment_text": "", "digests": { "md5": "781d1ff6fa4d08d4c06aa016c234f91d", "sha256": "aed609b6ff3f00d1a9e434f785dfc29e1ca6e2ca7810ce78a2da7e7cffc26657" }, "downloads": -1, "filename": "good-0.0.3-1.tar.gz", "has_sig": false, "md5_digest": "781d1ff6fa4d08d4c06aa016c234f91d", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 130807, "upload_time": "2014-09-05T15:07:51", "url": "https://files.pythonhosted.org/packages/f7/e0/cae285a424beccfdf2a3652f505619627d542a27e89f96219c37fd5bb773/good-0.0.3-1.tar.gz" } ], "0.0.3-2": [ { "comment_text": "", "digests": { "md5": "8d7934263e5a8bc03a5d16ed6903886c", "sha256": "12182177811b0dfe2276fa377a513eb5d47e4f2cdaeae412ad714e250a6fca20" }, "downloads": -1, "filename": "good-0.0.3_2-py27.py33.py34-none-any.whl", "has_sig": false, "md5_digest": "8d7934263e5a8bc03a5d16ed6903886c", "packagetype": "bdist_wheel", "python_version": "2.7", "requires_python": null, "size": 70079, "upload_time": "2014-09-05T15:08:51", "url": "https://files.pythonhosted.org/packages/e4/ac/c4b6f282b64a1503910194f0abbf090aa524f464cb9db3185eaca2911124/good-0.0.3_2-py27.py33.py34-none-any.whl" }, { "comment_text": "", "digests": { "md5": "8a46c2e3270bd216d894cecef664997c", "sha256": "248ae7042742cefef3001327cd9039a9c19d54ffff9d90ec69f5cc3844b229bd" }, "downloads": -1, "filename": "good-0.0.3-2.tar.gz", "has_sig": false, "md5_digest": "8a46c2e3270bd216d894cecef664997c", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 130827, "upload_time": "2014-09-05T15:08:48", "url": "https://files.pythonhosted.org/packages/af/36/cf879bc970c90da5a30e84e508c45e78f9a3c92bae97e8175a4a0ed176cc/good-0.0.3-2.tar.gz" } ], "0.0.3-3": [ { "comment_text": "", "digests": { "md5": "f816570efce5b4831a38ca38fc3a7e86", "sha256": "a323ebab3dd6425816d9efe382f29f3f07a0d177f6a8efa3a3361c5dde3232e1" }, "downloads": -1, "filename": "good-0.0.3_3-py27.py33.py34-none-any.whl", "has_sig": false, "md5_digest": "f816570efce5b4831a38ca38fc3a7e86", "packagetype": "bdist_wheel", "python_version": "2.7", "requires_python": null, "size": 70082, "upload_time": "2014-09-05T15:31:55", "url": "https://files.pythonhosted.org/packages/d0/86/be3befbc212486f0488f3280997c4ccbc73102292eaec9c3f21c64dc18ba/good-0.0.3_3-py27.py33.py34-none-any.whl" }, { "comment_text": "", "digests": { "md5": "87258e09c80245572c75db9f4b39aee4", "sha256": "8c573ae84ff009509f3565521d91ae1c5b009e7aeca7e347dd81b9b2a241cc28" }, "downloads": -1, "filename": "good-0.0.3-3.tar.gz", "has_sig": false, "md5_digest": "87258e09c80245572c75db9f4b39aee4", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 130822, "upload_time": "2014-09-05T15:31:52", "url": "https://files.pythonhosted.org/packages/46/57/8bc0474ce4003449b847e9efff722a5c152514d2bb4dcee41ea0b12d02d7/good-0.0.3-3.tar.gz" } ], "0.0.3-4": [ { "comment_text": "", "digests": { "md5": "345619e10bc0db79efab9db9fbee9130", "sha256": "f08e3955b78e33b8d16084e1bfed3217dccafffdd151f4deaa0fc2b5a744e9fe" }, "downloads": -1, "filename": "good-0.0.3_4-py27.py33.py34-none-any.whl", "has_sig": false, "md5_digest": "345619e10bc0db79efab9db9fbee9130", "packagetype": "bdist_wheel", "python_version": "2.7", "requires_python": null, "size": 70220, "upload_time": "2014-09-05T16:10:58", "url": "https://files.pythonhosted.org/packages/fa/0a/bb82f052b7ecd1da0b41153280e0d57e41818097a7ea0555ea0cf59cab80/good-0.0.3_4-py27.py33.py34-none-any.whl" }, { "comment_text": "", "digests": { "md5": "3178c42bfb7b62a84db04263a57d60a6", "sha256": "187fe14b1ae786f34c6272a552cd6ad61156f21c0e5b5b42e5cb4c644d1e25cb" }, "downloads": -1, "filename": "good-0.0.3-4.tar.gz", "has_sig": false, "md5_digest": "3178c42bfb7b62a84db04263a57d60a6", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 130944, "upload_time": "2014-09-05T16:10:54", "url": "https://files.pythonhosted.org/packages/36/d0/03e66ab65f55e2457bd6fc0770e4b033568f4f7b1bad0c2fe0703be1516d/good-0.0.3-4.tar.gz" } ], "0.0.3-5": [ { "comment_text": "", "digests": { "md5": "29b8e33d535082e67ffa62970c2a8d7b", "sha256": "b7deece6a0e41b61f7bb10031b00be3393c5acb53c38e0d4a5a572425e7cc6b9" }, "downloads": -1, "filename": "good-0.0.3_5-py27.py33.py34-none-any.whl", "has_sig": false, "md5_digest": "29b8e33d535082e67ffa62970c2a8d7b", "packagetype": "bdist_wheel", "python_version": "2.7", "requires_python": null, "size": 70224, "upload_time": "2014-09-05T17:08:27", "url": "https://files.pythonhosted.org/packages/7c/e5/5f82ecaa4aeea4f29cd807c725a1eb0841b5aad8f40e14308d9c44f40c69/good-0.0.3_5-py27.py33.py34-none-any.whl" }, { "comment_text": "", "digests": { "md5": "38f5ffde7431709b9ad11108d5bdfd63", "sha256": "df420ffd56ff396245e037aa5bd620a983a4931e21ecfd0fb41a408f629c4bae" }, "downloads": -1, "filename": "good-0.0.3-5.tar.gz", "has_sig": false, "md5_digest": "38f5ffde7431709b9ad11108d5bdfd63", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 130978, "upload_time": "2014-09-05T17:08:24", "url": "https://files.pythonhosted.org/packages/a2/3f/1ab9ca112a60968ae5bb46d60928e1b4b3253863f3e4c77701d3432b6c6f/good-0.0.3-5.tar.gz" } ], "0.0.4-0": [ { "comment_text": "", "digests": { "md5": "1df587726f822f2b62314e607b3f4831", "sha256": "a5d7a80b05dda907d1fddf3c6abc8fc1e9c423a8189fc7f1cf39a84081883f9b" }, "downloads": -1, "filename": "good-0.0.4_0-py27.py33.py34-none-any.whl", "has_sig": false, "md5_digest": "1df587726f822f2b62314e607b3f4831", "packagetype": "bdist_wheel", "python_version": "2.7", "requires_python": null, "size": 70243, "upload_time": "2014-09-05T19:19:40", "url": "https://files.pythonhosted.org/packages/e8/50/484fcd8091633f06a3618a831cd7c3815e39efc5574b1106699b996462e1/good-0.0.4_0-py27.py33.py34-none-any.whl" }, { "comment_text": "", "digests": { "md5": "ca8aa504d8c838cb40b4f195c907b0f3", "sha256": "53fe2201ddc28d3d8c1512c4f5982e82348b9b28fc4601d3e224b6bb76802b94" }, "downloads": -1, "filename": "good-0.0.4-0.tar.gz", "has_sig": false, "md5_digest": "ca8aa504d8c838cb40b4f195c907b0f3", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 130980, "upload_time": "2014-09-05T19:19:38", "url": "https://files.pythonhosted.org/packages/49/cb/574ab9944f78591041f82ef714a44a191bb197e51dc3a989fae8d17205b6/good-0.0.4-0.tar.gz" } ], "0.0.5-0": [ { "comment_text": "", "digests": { "md5": "61c7c0d8c43e3207c2bd3767c711b912", "sha256": "1823e3b0bfe855cc15836899ae13e3bbedf4db1d3cfb8f2ff58c148ba355b26c" }, "downloads": -1, "filename": "good-0.0.5_0-py27.py33.py34-none-any.whl", "has_sig": false, "md5_digest": "61c7c0d8c43e3207c2bd3767c711b912", "packagetype": "bdist_wheel", "python_version": "2.7", "requires_python": null, "size": 73815, "upload_time": "2014-09-06T03:03:39", "url": "https://files.pythonhosted.org/packages/7a/a1/b22b510eec328f954e5fb25607e20763afd204049436f345a25e1f0a064c/good-0.0.5_0-py27.py33.py34-none-any.whl" }, { "comment_text": "", "digests": { "md5": "86da8b484c8cda904a27aba85cb08038", "sha256": "09c08a8cc5046840bbe585178c5c9d57b533680deee9ac4e5d23d2328b62dd7c" }, "downloads": -1, "filename": "good-0.0.5-0.tar.gz", "has_sig": false, "md5_digest": "86da8b484c8cda904a27aba85cb08038", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 135768, "upload_time": "2014-09-06T03:03:37", "url": "https://files.pythonhosted.org/packages/14/1a/bdde271aeaf26bff0817b8ff0ea3cc7a6121380f3df2b0e5ed8cf1ebbd12/good-0.0.5-0.tar.gz" } ], "0.0.6-0": [ { "comment_text": "", "digests": { "md5": "faa324e9588dd00a2cfc6447289f3c86", "sha256": "7035d3a1b7f5e50615ec3400cd2b0238ad1cbb056978062013a1e378583e2d22" }, "downloads": -1, "filename": "good-0.0.6_0-py27.py33.py34-none-any.whl", "has_sig": false, "md5_digest": "faa324e9588dd00a2cfc6447289f3c86", "packagetype": "bdist_wheel", "python_version": "2.7", "requires_python": null, "size": 80167, "upload_time": "2014-09-07T02:45:10", "url": "https://files.pythonhosted.org/packages/40/03/9790274b809d128047fff29dca28802b829e21c4e4136ff8f5d91a122a00/good-0.0.6_0-py27.py33.py34-none-any.whl" }, { "comment_text": "", "digests": { "md5": "2d34c1bd0367ab9300b15b397cf6d029", "sha256": "ab94717d49bf73401088bb181fdc0b2db8999920a937d2dac0d545970d812eff" }, "downloads": -1, "filename": "good-0.0.6-0.tar.gz", "has_sig": false, "md5_digest": "2d34c1bd0367ab9300b15b397cf6d029", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 145611, "upload_time": "2014-09-07T02:45:06", "url": "https://files.pythonhosted.org/packages/4c/71/a49185684947054bb4d7246ef7c2c3668c4b14241d12e78f717212214e34/good-0.0.6-0.tar.gz" } ], "0.0.6-1": [ { "comment_text": "", "digests": { "md5": "d55874f37b7aa1c0d0d2aacf934d2cc2", "sha256": "45e4e286bfc8403ae26b6d416ad8950858f86f21961916e718700fe9e84931c6" }, "downloads": -1, "filename": "good-0.0.6_1-py27.py33.py34-none-any.whl", "has_sig": false, "md5_digest": "d55874f37b7aa1c0d0d2aacf934d2cc2", "packagetype": "bdist_wheel", "python_version": "2.7", "requires_python": null, "size": 48095, "upload_time": "2014-09-07T16:06:26", "url": "https://files.pythonhosted.org/packages/c2/fc/effd3f7f4b5b8328f6b3d5126dcecbfa6fe6a0ee0a691eb48b8c2dc3aa1b/good-0.0.6_1-py27.py33.py34-none-any.whl" }, { "comment_text": "", "digests": { "md5": "9ce70b7c31fdacab86d4c61d140688fd", "sha256": "fa087f2b4f179414da274ae219e79eaac9449178b91ab0e914a189a155972258" }, "downloads": -1, "filename": "good-0.0.6-1.tar.gz", "has_sig": false, "md5_digest": "9ce70b7c31fdacab86d4c61d140688fd", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 81642, "upload_time": "2014-09-07T16:06:23", "url": "https://files.pythonhosted.org/packages/f8/cd/21f51c152384085da0d6e359821e8af26236396de0f6e09c3f10fd10b6ca/good-0.0.6-1.tar.gz" } ], "0.0.6-2": [ { "comment_text": "", "digests": { "md5": "d83b1f022f15c822557386f55ea90569", "sha256": "fdd1e6a65a3dabed969af9a65d69433acd973737c5bef7bbceb64895adfa6d48" }, "downloads": -1, "filename": "good-0.0.6_2-py27.py33.py34-none-any.whl", "has_sig": false, "md5_digest": "d83b1f022f15c822557386f55ea90569", "packagetype": "bdist_wheel", "python_version": "2.7", "requires_python": null, "size": 80739, "upload_time": "2014-09-07T17:05:55", "url": "https://files.pythonhosted.org/packages/05/4f/5a1488e72af67c105f3f27ff6f4610be0aa8657b475e59c828873e61f8db/good-0.0.6_2-py27.py33.py34-none-any.whl" }, { "comment_text": "", "digests": { "md5": "38058e813c8177db213a5d51c58738d4", "sha256": "8d6858a48a2dddf2b87c02cfc94beb50966bb7d831f2a55467acf02e642da0a5" }, "downloads": -1, "filename": "good-0.0.6-2.tar.gz", "has_sig": false, "md5_digest": "38058e813c8177db213a5d51c58738d4", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 146517, "upload_time": "2014-09-07T17:05:51", "url": "https://files.pythonhosted.org/packages/90/40/009bec830c795a6e8131c4ff53dba284a1e243d2b1ae12332681212314a5/good-0.0.6-2.tar.gz" } ], "0.0.6-3": [ { "comment_text": "", "digests": { "md5": "cffabad6c3120b214d93d5a13dc2ed8c", "sha256": "e02b6069173e5a99fcc9e8a375469f27975d9b283cb61661c33d73a871405cff" }, "downloads": -1, "filename": "good-0.0.6_3-py27.py33.py34-none-any.whl", "has_sig": false, "md5_digest": "cffabad6c3120b214d93d5a13dc2ed8c", "packagetype": "bdist_wheel", "python_version": "2.7", "requires_python": null, "size": 82267, "upload_time": "2014-09-07T18:08:21", "url": "https://files.pythonhosted.org/packages/80/79/0e35b36b997f4d042426842d589757c434d5dca6861d28c986e479201da8/good-0.0.6_3-py27.py33.py34-none-any.whl" }, { "comment_text": "", "digests": { "md5": "5c336813f73578b0cf0f44e9525b38a1", "sha256": "7f314afb8de847a81a9780483ca51aebe97a58d1a557642c9870127988c1f94a" }, "downloads": -1, "filename": "good-0.0.6-3.tar.gz", "has_sig": false, "md5_digest": "5c336813f73578b0cf0f44e9525b38a1", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 149164, "upload_time": "2014-09-07T18:08:18", "url": "https://files.pythonhosted.org/packages/a9/3d/8456b52d14d073f760346b05bc166999386676d362ad2e9059603996a328/good-0.0.6-3.tar.gz" } ], "0.0.7-0": [ { "comment_text": "", "digests": { "md5": "616519007830ad8af737411137aec021", "sha256": "bd1e2e51d2775fc759c54946179f77df63d4f4b5fc6eb7bdc5081f0b64943e82" }, "downloads": -1, "filename": "good-0.0.7_0-py27.py33.py34-none-any.whl", "has_sig": false, "md5_digest": "616519007830ad8af737411137aec021", "packagetype": "bdist_wheel", "python_version": "2.7", "requires_python": null, "size": 82418, "upload_time": "2014-09-20T19:42:40", "url": "https://files.pythonhosted.org/packages/b8/0e/88f8c8e3396c55fb2cedb9898c287e6dd3fe63d004fca67c3f8db12f3221/good-0.0.7_0-py27.py33.py34-none-any.whl" }, { "comment_text": "", "digests": { "md5": "20a81abb640c405714f175d23a402eb2", "sha256": "315490fc920828f496e046d96784260dba94c06357b4a8ddac91174ea2152c98" }, "downloads": -1, "filename": "good-0.0.7-0.tar.gz", "has_sig": false, "md5_digest": "20a81abb640c405714f175d23a402eb2", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 149330, "upload_time": "2014-09-20T19:42:36", "url": "https://files.pythonhosted.org/packages/e0/8c/2814782f3d0ad98b2d40acf36ccd86113ec32052312b955dbda279c7814d/good-0.0.7-0.tar.gz" } ], "0.0.7.post1": [ { "comment_text": "", "digests": { "md5": "f9b591e2316e3d2ed67b031d70967278", "sha256": "3c646a8b3a8dfdbe9d7f6b58e4281ef9bc30b996d462d01bf43bffb15aef7ef7" }, "downloads": -1, "filename": "good-0.0.7.post1-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "f9b591e2316e3d2ed67b031d70967278", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 65376, "upload_time": "2018-12-28T13:17:51", "url": "https://files.pythonhosted.org/packages/1f/e6/21fd8e676ebfec5aa4ed65893739d23bb414855fa068f2a4874493fd26cd/good-0.0.7.post1-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "e9d608fd2b09264d187d842ecc79b9b0", "sha256": "42ddd6d3425329ec2482adf32a5ac41fb748112dd340f574e8607e9b3caaf844" }, "downloads": -1, "filename": "good-0.0.7.post1.tar.gz", "has_sig": false, "md5_digest": "e9d608fd2b09264d187d842ecc79b9b0", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 147643, "upload_time": "2018-12-28T13:17:54", "url": "https://files.pythonhosted.org/packages/3d/fe/a63b786b63eabfa065d729c66aaaac802e51ec5af2d89af77c903bf8b769/good-0.0.7.post1.tar.gz" } ], "0.0.8": [ { "comment_text": "", "digests": { "md5": "fea32d68068e329ada7eac82813066ce", "sha256": "d15e9cf052370dc1139dcc219deca0ed77ccc912b62990e093dafe68766a23cd" }, "downloads": -1, "filename": "good-0.0.8-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "fea32d68068e329ada7eac82813066ce", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 64951, "upload_time": "2019-10-17T14:07:11", "url": "https://files.pythonhosted.org/packages/4e/ce/512f595ddc674eaf50016c6a65868f55ad8dd6a5a84d77ad4d6fdd1d3a62/good-0.0.8-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "23896b28502c6278d15e4048b502ec59", "sha256": "0d15b52f89f553be8960bd86bc3caea6ba8ab5a48daf47244755159df12c808a" }, "downloads": -1, "filename": "good-0.0.8.tar.gz", "has_sig": false, "md5_digest": "23896b28502c6278d15e4048b502ec59", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 89666, "upload_time": "2019-10-17T14:07:16", "url": "https://files.pythonhosted.org/packages/73/6a/fc20f76e20f4c0b7993d4dd0d12d9cd465a84ab34bccbc4f6ca082aca262/good-0.0.8.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "fea32d68068e329ada7eac82813066ce", "sha256": "d15e9cf052370dc1139dcc219deca0ed77ccc912b62990e093dafe68766a23cd" }, "downloads": -1, "filename": "good-0.0.8-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "fea32d68068e329ada7eac82813066ce", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 64951, "upload_time": "2019-10-17T14:07:11", "url": "https://files.pythonhosted.org/packages/4e/ce/512f595ddc674eaf50016c6a65868f55ad8dd6a5a84d77ad4d6fdd1d3a62/good-0.0.8-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "23896b28502c6278d15e4048b502ec59", "sha256": "0d15b52f89f553be8960bd86bc3caea6ba8ab5a48daf47244755159df12c808a" }, "downloads": -1, "filename": "good-0.0.8.tar.gz", "has_sig": false, "md5_digest": "23896b28502c6278d15e4048b502ec59", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 89666, "upload_time": "2019-10-17T14:07:16", "url": "https://files.pythonhosted.org/packages/73/6a/fc20f76e20f4c0b7993d4dd0d12d9cd465a84ab34bccbc4f6ca082aca262/good-0.0.8.tar.gz" } ] }