You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			278 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			278 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Python
		
	
#    ___
 | 
						|
#    \./     DANGER: This project implements some code generation
 | 
						|
# .--.O.--.          techniques involving string concatenation.
 | 
						|
#  \/   \/           If you look at it, you might die.
 | 
						|
#
 | 
						|
 | 
						|
r"""
 | 
						|
Installation
 | 
						|
************
 | 
						|
 | 
						|
.. code-block:: bash
 | 
						|
 | 
						|
    pip install fastjsonschema
 | 
						|
 | 
						|
Support only for Python 3.3 and higher.
 | 
						|
 | 
						|
About
 | 
						|
*****
 | 
						|
 | 
						|
``fastjsonschema`` implements validation of JSON documents by JSON schema.
 | 
						|
The library implements JSON schema drafts 04, 06, and 07. The main purpose is
 | 
						|
to have a really fast implementation. See some numbers:
 | 
						|
 | 
						|
 * Probably the most popular, ``jsonschema``, can take up to 5 seconds for valid
 | 
						|
   inputs and 1.2 seconds for invalid inputs.
 | 
						|
 * Second most popular, ``json-spec``, is even worse with up to 7.2 and 1.7 seconds.
 | 
						|
 * Last ``validictory``, now deprecated, is much better with 370 or 23 milliseconds,
 | 
						|
   but it does not follow all standards, and it can be still slow for some purposes.
 | 
						|
 | 
						|
With this library you can gain big improvements as ``fastjsonschema`` takes
 | 
						|
only about 25 milliseconds for valid inputs and 2 milliseconds for invalid ones.
 | 
						|
Pretty amazing, right? :-)
 | 
						|
 | 
						|
Technically it works by generating the most stupid code on the fly, which is fast but
 | 
						|
is hard to write by hand. The best efficiency is achieved when a validator is compiled
 | 
						|
once and used many times, of course. It works similarly like regular expressions. But
 | 
						|
you can also generate the code to a file, which is even slightly faster.
 | 
						|
 | 
						|
You can run the performance benchmarks on your computer or server with the included
 | 
						|
script:
 | 
						|
 | 
						|
.. code-block:: bash
 | 
						|
 | 
						|
    $ make performance
 | 
						|
    fast_compiled                  valid      ==>  0.0993900
 | 
						|
    fast_compiled                  invalid    ==>  0.0041089
 | 
						|
    fast_compiled_without_exc      valid      ==>  0.0465258
 | 
						|
    fast_compiled_without_exc      invalid    ==>  0.0023688
 | 
						|
    fast_file                      valid      ==>  0.0989483
 | 
						|
    fast_file                      invalid    ==>  0.0041104
 | 
						|
    fast_not_compiled              valid      ==> 11.9572681
 | 
						|
    fast_not_compiled              invalid    ==>  2.9512092
 | 
						|
    jsonschema                     valid      ==>  5.2233240
 | 
						|
    jsonschema                     invalid    ==>  1.3227916
 | 
						|
    jsonschema_compiled            valid      ==>  0.4447982
 | 
						|
    jsonschema_compiled            invalid    ==>  0.0231333
 | 
						|
    jsonspec                       valid      ==>  4.1450569
 | 
						|
    jsonspec                       invalid    ==>  1.0485777
 | 
						|
    validictory                    valid      ==>  0.2730411
 | 
						|
    validictory                    invalid    ==>  0.0183669
 | 
						|
 | 
						|
This library follows and implements `JSON schema draft-04, draft-06, and draft-07
 | 
						|
<http://json-schema.org>`_. Sometimes it's not perfectly clear, so I recommend also
 | 
						|
check out this `understanding JSON schema <https://spacetelescope.github.io/understanding-json-schema>`_.
 | 
						|
 | 
						|
Note that there are some differences compared to JSON schema standard:
 | 
						|
 | 
						|
 * Regular expressions are full Python ones, not only what JSON schema allows. It's easier
 | 
						|
   to allow everything, and also it's faster to compile without limits. So keep in mind that when
 | 
						|
   you will use a more advanced regular expression, it may not work with other libraries or in
 | 
						|
   other languages.
 | 
						|
 * Because Python matches new line for a dollar in regular expressions (``a$`` matches ``a`` and ``a\\n``),
 | 
						|
   instead of ``$`` is used ``\Z`` and all dollars in your regular expression are changed to ``\\Z``
 | 
						|
   as well. When you want to use dollar as regular character, you have to escape it (``\$``).
 | 
						|
 * JSON schema says you can use keyword ``default`` for providing default values. This implementation
 | 
						|
   uses that and always returns transformed input data.
 | 
						|
 | 
						|
Usage
 | 
						|
*****
 | 
						|
 | 
						|
.. code-block:: python
 | 
						|
 | 
						|
    import fastjsonschema
 | 
						|
 | 
						|
    point_schema = {
 | 
						|
        "type": "object",
 | 
						|
        "properties": {
 | 
						|
            "x": {
 | 
						|
                "type": "number",
 | 
						|
            },
 | 
						|
            "y": {
 | 
						|
                "type": "number",
 | 
						|
            },
 | 
						|
        },
 | 
						|
        "required": ["x", "y"],
 | 
						|
        "additionalProperties": False,
 | 
						|
    }
 | 
						|
 | 
						|
    point_validator = fastjsonschema.compile(point_schema)
 | 
						|
    try:
 | 
						|
        point_validator({"x": 1.0, "y": 2.0})
 | 
						|
    except fastjsonschema.JsonSchemaException as e:
 | 
						|
        print(f"Data failed validation: {e}")
 | 
						|
 | 
						|
API
 | 
						|
***
 | 
						|
"""
 | 
						|
from functools import partial, update_wrapper
 | 
						|
 | 
						|
from .draft04 import CodeGeneratorDraft04
 | 
						|
from .draft06 import CodeGeneratorDraft06
 | 
						|
from .draft07 import CodeGeneratorDraft07
 | 
						|
from .exceptions import JsonSchemaException, JsonSchemaValueException, JsonSchemaDefinitionException
 | 
						|
from .ref_resolver import RefResolver
 | 
						|
from .version import VERSION
 | 
						|
 | 
						|
__all__ = (
 | 
						|
    'VERSION',
 | 
						|
    'JsonSchemaException',
 | 
						|
    'JsonSchemaValueException',
 | 
						|
    'JsonSchemaDefinitionException',
 | 
						|
    'validate',
 | 
						|
    'compile',
 | 
						|
    'compile_to_code',
 | 
						|
)
 | 
						|
 | 
						|
 | 
						|
def validate(definition, data, handlers={}, formats={}, use_default=True, use_formats=True, detailed_exceptions=True):
 | 
						|
    """
 | 
						|
    Validation function for lazy programmers or for use cases when you need
 | 
						|
    to call validation only once, so you do not have to compile it first.
 | 
						|
    Use it only when you do not care about performance (even though it will
 | 
						|
    be still faster than alternative implementations).
 | 
						|
 | 
						|
    .. code-block:: python
 | 
						|
 | 
						|
        import fastjsonschema
 | 
						|
 | 
						|
        fastjsonschema.validate({'type': 'string'}, 'hello')
 | 
						|
        # same as: compile({'type': 'string'})('hello')
 | 
						|
 | 
						|
    Preferred is to use :any:`compile` function.
 | 
						|
    """
 | 
						|
    return compile(definition, handlers, formats, use_default, use_formats, detailed_exceptions)(data)
 | 
						|
 | 
						|
 | 
						|
#TODO: Change use_default to False when upgrading to version 3.
 | 
						|
# pylint: disable=redefined-builtin,dangerous-default-value,exec-used
 | 
						|
def compile(definition, handlers={}, formats={}, use_default=True, use_formats=True, detailed_exceptions=True):
 | 
						|
    """
 | 
						|
    Generates validation function for validating JSON schema passed in ``definition``.
 | 
						|
    Example:
 | 
						|
 | 
						|
    .. code-block:: python
 | 
						|
 | 
						|
        import fastjsonschema
 | 
						|
 | 
						|
        validate = fastjsonschema.compile({'type': 'string'})
 | 
						|
        validate('hello')
 | 
						|
 | 
						|
    This implementation supports keyword ``default`` (can be turned off
 | 
						|
    by passing `use_default=False`):
 | 
						|
 | 
						|
    .. code-block:: python
 | 
						|
 | 
						|
        validate = fastjsonschema.compile({
 | 
						|
            'type': 'object',
 | 
						|
            'properties': {
 | 
						|
                'a': {'type': 'number', 'default': 42},
 | 
						|
            },
 | 
						|
        })
 | 
						|
 | 
						|
        data = validate({})
 | 
						|
        assert data == {'a': 42}
 | 
						|
 | 
						|
    Supported implementations are draft-04, draft-06 and draft-07. Which version
 | 
						|
    should be used is determined by `$draft` in your ``definition``. When not
 | 
						|
    specified, the latest implementation is used (draft-07).
 | 
						|
 | 
						|
    .. code-block:: python
 | 
						|
 | 
						|
        validate = fastjsonschema.compile({
 | 
						|
            '$schema': 'http://json-schema.org/draft-04/schema',
 | 
						|
            'type': 'number',
 | 
						|
        })
 | 
						|
 | 
						|
    You can pass mapping from URI to function that should be used to retrieve
 | 
						|
    remote schemes used in your ``definition`` in parameter ``handlers``.
 | 
						|
 | 
						|
    Also, you can pass mapping for custom formats. Key is the name of your
 | 
						|
    formatter and value can be regular expression, which will be compiled or
 | 
						|
    callback returning `bool` (or you can raise your own exception).
 | 
						|
 | 
						|
    .. code-block:: python
 | 
						|
 | 
						|
        validate = fastjsonschema.compile(definition, formats={
 | 
						|
            'foo': r'foo|bar',
 | 
						|
            'bar': lambda value: value in ('foo', 'bar'),
 | 
						|
        })
 | 
						|
 | 
						|
    Note that formats are automatically used as assertions. It can be turned
 | 
						|
    off by passing `use_formats=False`. When disabled, custom formats are
 | 
						|
    disabled as well. (Added in 2.19.0.)
 | 
						|
 | 
						|
    If you don't need detailed exceptions, you can turn the details off and gain
 | 
						|
    additional performance by passing `detailed_exceptions=False`.
 | 
						|
 | 
						|
    Exception :any:`JsonSchemaDefinitionException` is raised when generating the
 | 
						|
    code fails (bad definition).
 | 
						|
 | 
						|
    Exception :any:`JsonSchemaValueException` is raised from generated function when
 | 
						|
    validation fails (data do not follow the definition).
 | 
						|
    """
 | 
						|
    resolver, code_generator = _factory(definition, handlers, formats, use_default, use_formats, detailed_exceptions)
 | 
						|
    global_state = code_generator.global_state
 | 
						|
    # Do not pass local state so it can recursively call itself.
 | 
						|
    exec(code_generator.func_code, global_state)
 | 
						|
    func = global_state[resolver.get_scope_name()]
 | 
						|
    if formats:
 | 
						|
        return update_wrapper(partial(func, custom_formats=formats), func)
 | 
						|
    return func
 | 
						|
 | 
						|
 | 
						|
# pylint: disable=dangerous-default-value
 | 
						|
def compile_to_code(definition, handlers={}, formats={}, use_default=True, use_formats=True, detailed_exceptions=True):
 | 
						|
    """
 | 
						|
    Generates validation code for validating JSON schema passed in ``definition``.
 | 
						|
    Example:
 | 
						|
 | 
						|
    .. code-block:: python
 | 
						|
 | 
						|
        import fastjsonschema
 | 
						|
 | 
						|
        code = fastjsonschema.compile_to_code({'type': 'string'})
 | 
						|
        with open('your_file.py', 'w') as f:
 | 
						|
            f.write(code)
 | 
						|
 | 
						|
    You can also use it as a script:
 | 
						|
 | 
						|
    .. code-block:: bash
 | 
						|
 | 
						|
        echo "{'type': 'string'}" | python3 -m fastjsonschema > your_file.py
 | 
						|
        python3 -m fastjsonschema "{'type': 'string'}" > your_file.py
 | 
						|
 | 
						|
    Exception :any:`JsonSchemaDefinitionException` is raised when generating the
 | 
						|
    code fails (bad definition).
 | 
						|
    """
 | 
						|
    _, code_generator = _factory(definition, handlers, formats, use_default, use_formats, detailed_exceptions)
 | 
						|
    return (
 | 
						|
        'VERSION = "' + VERSION + '"\n' +
 | 
						|
        code_generator.global_state_code + '\n' +
 | 
						|
        code_generator.func_code
 | 
						|
    )
 | 
						|
 | 
						|
 | 
						|
def _factory(definition, handlers, formats={}, use_default=True, use_formats=True, detailed_exceptions=True):
 | 
						|
    resolver = RefResolver.from_schema(definition, handlers=handlers, store={})
 | 
						|
    code_generator = _get_code_generator_class(definition)(
 | 
						|
        definition,
 | 
						|
        resolver=resolver,
 | 
						|
        formats=formats,
 | 
						|
        use_default=use_default,
 | 
						|
        use_formats=use_formats,
 | 
						|
        detailed_exceptions=detailed_exceptions,
 | 
						|
    )
 | 
						|
    return resolver, code_generator
 | 
						|
 | 
						|
 | 
						|
def _get_code_generator_class(schema):
 | 
						|
    # Schema in from draft-06 can be just the boolean value.
 | 
						|
    if isinstance(schema, dict):
 | 
						|
        schema_version = schema.get('$schema', '')
 | 
						|
        if 'draft-04' in schema_version:
 | 
						|
            return CodeGeneratorDraft04
 | 
						|
        if 'draft-06' in schema_version:
 | 
						|
            return CodeGeneratorDraft06
 | 
						|
    return CodeGeneratorDraft07
 |