├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.rst ├── jsonschema_model.py ├── setup.cfg ├── setup.py ├── test_jsonschema_model.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | .tox 3 | build 4 | dist 5 | *.egg 6 | .local 7 | .coverage 8 | AUTHORS 9 | ChangeLog 10 | __pycache__ 11 | *.egg-info 12 | *.pyc 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | python: 2.7 4 | env: 5 | - TOX_ENV=flake8 6 | - TOX_ENV=py2 7 | - TOX_ENV=py3 8 | install: 9 | - pip install tox 10 | script: 11 | - tox -e $TOX_ENV 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 by Philippe Pepiot and contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | * Neither the name of Philippe Pepiot nor the names of its contributors 13 | may be used to endorse or promote products derived from this software 14 | without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 18 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 19 | PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS 20 | BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 21 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 22 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 23 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 24 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 25 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 | POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include jsonschema_model.py 2 | include test_jsonschema_model.py 3 | include README.rst 4 | include LICENSE 5 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Json Schema Model 2 | ================= 3 | 4 | .. image:: https://travis-ci.org/philpep/jsonschema-model.svg?branch=master 5 | :target: https://travis-ci.org/philpep/jsonschema-model 6 | :alt: Build status 7 | 8 | .. image:: https://pypip.in/version/jsonschema-model/badge.png 9 | :target: https://pypi.python.org/pypi/jsonschema-model/ 10 | :alt: Latest Version 11 | 12 | .. image:: https://pypip.in/license/jsonschema-model/badge.png 13 | :target: https://pypi.python.org/pypi/jsonschema-model/ 14 | :alt: License 15 | 16 | 17 | Build python objects using JSON schemas:: 18 | 19 | >>> import jsonschema_model 20 | >>> Model = jsonschema_model.model_factory({ 21 | ... "name": "Model", 22 | ... "properties": { 23 | ... "foo": {"type": "string"}, 24 | ... "bar": {"type": "array", "items": { 25 | ... "type": "object", 26 | ... "name": "Bar", 27 | ... "properties": { 28 | ... "zaz": {"type": "string"}, 29 | ... }, 30 | ... }}, 31 | ... }}) 32 | 33 | # Simple object creation 34 | >>> obj = Model(foo="bar") 35 | >>> assert obj == {"foo": "bar"} 36 | 37 | # Nested and array are implemented 38 | # HINT: Use add() instead of append() 39 | >>> obj.bar.add(zaz="qux") 40 | {'zaz': 'qux'} 41 | >>> assert obj == {'foo': 'bar', 'bar': [{'zaz': 'qux'}]} 42 | 43 | # You can access via attribute or via dict like interface 44 | >>> obj["bar"][0].zaz 45 | 'qux' 46 | 47 | # Array have a special get_or_create() method 48 | # to avoid dupplicates within an array 49 | >>> obj.bar.get_or_create(zaz="xuq") 50 | {'zaz': 'xuq'} 51 | >>> obj.bar 52 | [{'zaz': 'qux'}, {'zaz': 'xuq'}] 53 | >>> obj.bar.get_or_create(zaz="xuq") 54 | {'zaz': 'xuq'} 55 | >>> obj.bar 56 | [{'zaz': 'qux'}, {'zaz': 'xuq'}] 57 | -------------------------------------------------------------------------------- /jsonschema_model.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | 3 | from __future__ import unicode_literals 4 | 5 | 6 | class MultipleItemFound(RuntimeError): 7 | pass 8 | 9 | 10 | class Array(list): 11 | Model = None 12 | 13 | def __init__(self, *args, **kwargs): 14 | list.__init__(self) 15 | for e in list(*args, **kwargs): 16 | self.add(e) 17 | 18 | def add(self, *args, **kwargs): 19 | if self.Model is not None: 20 | obj = self.Model(*args, **kwargs) 21 | else: 22 | obj = args[0] 23 | self.append(obj) 24 | return obj 25 | 26 | def get_or_create(self, *args, **kwargs): 27 | match = [] 28 | if ( 29 | all([args, kwargs]) or 30 | not any([args, kwargs]) or 31 | (args and len(args) != 1) 32 | ): 33 | raise RuntimeError("Invalid usage of get_or_create()") 34 | for obj in self: 35 | if kwargs and all(getattr(obj, k) == v for k, v in kwargs.items()): 36 | match.append(obj) 37 | elif args and args[0] == obj: 38 | match.append(obj) 39 | if len(match) > 1: 40 | raise MultipleItemFound 41 | elif len(match) == 0: 42 | return self.add(*args, **kwargs) 43 | else: # 1 item 44 | return match[0] 45 | 46 | 47 | class Model(dict): 48 | _schema = None 49 | 50 | def __init__(self, *args, **kwargs): 51 | dict.__init__(self) 52 | for k, v in dict(*args, **kwargs).items(): 53 | self.__setitem__(k, v) 54 | 55 | def __setitem__(self, key, value): 56 | try: 57 | model = self._get_model(key) 58 | except KeyError: 59 | pass 60 | else: 61 | if model is not None: 62 | value = model(value) 63 | dict.__setitem__(self, key, value) 64 | 65 | def update(self, **kwargs): 66 | for k, v in kwargs.items(): 67 | self.__setitem__(k, v) 68 | 69 | def _get_model(self, key): 70 | schema = self._schema["properties"].get(key) 71 | 72 | if schema is None: 73 | raise KeyError(key) 74 | 75 | if schema["type"] == "object": 76 | return model_factory(schema) 77 | elif schema["type"] == "array": 78 | if schema["items"]["type"] == "object": 79 | ArrayModel = model_factory(schema["items"]) 80 | else: 81 | ArrayModel = None 82 | return type(str("Array"), (Array,), {"Model": ArrayModel}) 83 | else: 84 | return None 85 | 86 | def __getitem__(self, key): 87 | try: 88 | return dict.__getitem__(self, key) 89 | except KeyError: 90 | model = self._get_model(key) 91 | if model is not None: 92 | value = model() 93 | dict.__setitem__(self, key, value) 94 | return value 95 | 96 | def __getattr__(self, key): 97 | try: 98 | return self.__getitem__(key) 99 | except KeyError: 100 | raise AttributeError(key) 101 | 102 | def __setattr__(self, key, value): 103 | return self.__setitem__(key, value) 104 | 105 | 106 | def model_factory(schema): 107 | name = schema.get("name", "Object") 108 | return type(str(name), (Model,), {"_schema": schema}) 109 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal = 1 3 | 4 | [metadata] 5 | name = jsonschema-model 6 | author = Philippe Pepiot 7 | author-email = phil@philpep.org 8 | summary = Json schema Model 9 | description-file = README.rst 10 | home-page = https://github.com/philpep/jsonschema-model 11 | license = BSD 12 | 13 | [tool:pytest] 14 | norecursedirs = .tox .git *.egg build .local 15 | addopts = --doctest-glob='*.rst' 16 | 17 | [flake8] 18 | ignore = H802,H301,H101,H306,H302,H904,H305,E226,H405,E123,H236 19 | exclude = .tox,build,*.egg 20 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import setuptools 4 | 5 | setuptools.setup( 6 | py_modules=["jsonschema_model"], 7 | setup_requires=["pbr"], 8 | pbr=True 9 | ) 10 | -------------------------------------------------------------------------------- /test_jsonschema_model.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | 3 | from __future__ import unicode_literals 4 | 5 | import copy 6 | import pytest 7 | from jsonschema_model import model_factory, MultipleItemFound 8 | 9 | 10 | @pytest.fixture 11 | def Model(): 12 | # evil nested schema 13 | schema = { 14 | "name": "Model", 15 | "type": "object", 16 | "properties": { 17 | "foo": {"type": "string"}, 18 | "bar": {"type": "array", "items": {"type": "string"}}, 19 | } 20 | } 21 | orig = copy.deepcopy(schema) 22 | schema["properties"]["zaz"] = orig 23 | schema["properties"]["qux"] = {"type": "array", "items": orig} 24 | return model_factory(schema) 25 | 26 | 27 | def test_init(Model): 28 | obj = Model(foo="bar") 29 | assert obj == {"foo": "bar"} 30 | 31 | 32 | def test_error(Model): 33 | obj = Model() 34 | with pytest.raises(AttributeError): 35 | obj.attr 36 | with pytest.raises(KeyError): 37 | obj["attr"] 38 | 39 | 40 | def test_basic(Model): 41 | obj = Model() 42 | assert obj.foo is None 43 | assert obj == {} 44 | 45 | obj.foo = None 46 | assert obj == {"foo": None} 47 | 48 | obj.foo = "bar" 49 | assert obj == {"foo": "bar"} 50 | 51 | obj["foo"] = "zaz" 52 | assert obj == {"foo": "zaz"} 53 | 54 | 55 | def test_simple_array(Model): 56 | obj = Model() 57 | obj.bar.add("foo") 58 | obj.bar.add("bar") 59 | assert obj == {"bar": ["foo", "bar"]} 60 | 61 | 62 | def test_update_array(Model): 63 | obj = Model() 64 | obj.update(bar=["foo"]) 65 | obj.bar.add("bar") 66 | assert obj == {"bar": ["foo", "bar"]} 67 | 68 | 69 | def test_nested(Model): 70 | obj = Model() 71 | assert obj.zaz == {} 72 | assert obj == {"zaz": {}} 73 | obj["zaz"].foo = "bar" 74 | assert obj == {"zaz": {"foo": "bar"}} 75 | 76 | 77 | def test_nested_array(Model): 78 | obj = Model() 79 | obj.qux.add(foo="bar") 80 | assert obj == {"qux": [{"foo": "bar"}]} 81 | 82 | 83 | def test_init_fill(Model): 84 | data = { 85 | "foo": "a", 86 | "bar": ["b", "c"], 87 | "zaz": { 88 | "bar": ["d"], 89 | }, 90 | "qux": [{"foo": "e"}, {"bar": ["f"]}], 91 | } 92 | obj = Model(**data) 93 | assert obj.foo == "a" 94 | assert obj.bar == ["b", "c"] 95 | assert obj.zaz.bar == ["d"] 96 | assert obj.qux[0].foo == "e" 97 | assert obj.qux[1].bar == ["f"] 98 | assert obj == data 99 | 100 | g = obj.qux.add(foo="g", bar=["h"]) 101 | assert obj.qux[2] == {"foo": "g", "bar": ["h"]} 102 | g.bar.add("i") 103 | assert obj.qux[2] == {"foo": "g", "bar": ["h", "i"]} 104 | 105 | 106 | def test_name(): 107 | model = model_factory({ 108 | "type": "object", 109 | "name": "FooObject", 110 | "properties": { 111 | "foo": { 112 | "type": "object", 113 | "properties": { 114 | "foo": {"type": "string"}, 115 | }, 116 | }, 117 | }, 118 | }) 119 | obj = model(foo={"foo": "bar"}) 120 | assert obj.__class__.__name__ == "FooObject" 121 | assert obj.foo.__class__.__name__ == "Object" 122 | 123 | 124 | def test_get_or_create_invalid(): 125 | model = model_factory({ 126 | "type": "object", 127 | "properties": { 128 | "l": {"type": "array", "items": {"type": "string"}}, 129 | }, 130 | }) 131 | obj = model() 132 | with pytest.raises(RuntimeError): 133 | obj.l.get_or_create("foo", "bar") 134 | with pytest.raises(RuntimeError): 135 | obj.l.get_or_create("foo", a="bar") 136 | with pytest.raises(MultipleItemFound): 137 | obj.l.add("foo") 138 | obj.l.add("foo") 139 | obj.l.get_or_create("foo") 140 | 141 | 142 | def test_get_or_create_simple(): 143 | model = model_factory({ 144 | "type": "object", 145 | "properties": { 146 | "l": {"type": "array", "items": {"type": "string"}}, 147 | }, 148 | }) 149 | obj = model() 150 | obj.l.get_or_create("foo") 151 | obj.l.get_or_create("foo") 152 | assert obj == {"l": ["foo"]} 153 | obj.l.get_or_create("bar") 154 | assert obj == {"l": ["foo", "bar"]} 155 | 156 | 157 | def test_get_or_create_object(): 158 | model = model_factory({ 159 | "type": "object", 160 | "properties": { 161 | "l": { 162 | "type": "array", 163 | "items": { 164 | "type": "object", 165 | "properties": { 166 | "a": {"type": "string"}, 167 | "b": {"type": "string"}, 168 | }, 169 | }, 170 | }, 171 | }, 172 | }) 173 | obj = model() 174 | obj.l.get_or_create(a="foo") 175 | assert obj == {"l": [{"a": "foo"}]} 176 | obj.l.get_or_create(a="foo") 177 | assert obj == {"l": [{"a": "foo"}]} 178 | obj.l.get_or_create(a="foo", b="bar") 179 | assert obj == {"l": [{"a": "foo"}, {"a": "foo", "b": "bar"}]} 180 | obj.l.get_or_create(a="foo", b="bar") 181 | assert obj == {"l": [{"a": "foo"}, {"a": "foo", "b": "bar"}]} 182 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist=py2,py3,flake8 3 | 4 | [testenv] 5 | deps=pytest 6 | commands=py.test 7 | 8 | [testenv:flake8] 9 | basepython=python3 10 | deps=hacking 11 | commands=flake8 12 | --------------------------------------------------------------------------------