Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New features and improvements #39

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
Changes
=======

0.7.1 (Aug 26, 2016)
--------------------

* Added new type: URL.
* Added new property to fields: Description.
* Added the optional use os UstraJSON, ultra fast JSON serializer.
* Added 'properties' classmethod to Models. With it, you can get the fields defined in a Model, without instantiate it.
* Improved validation. Now, types derived from basic types has strong checks: int, fload, bool, string, list.
* Improved compatibility with Python 3.


0.7.0 (Dec 3, 2014)
-------------------

Expand Down
69 changes: 69 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ Booby: data modeling and validation
:target: http://travis-ci.org/jaimegildesagredo/booby
:alt: Build status

**This project is a fork of official project*** with some new features.

Booby is a standalone data `modeling` and `validation` library written in Python. Booby is under active development (visit `this blog post <http://jaimegildesagredo.github.io/2014/01/04/booby-05-introducing-inspection-api.html>`_ for more info and the roadmap) and licensed under the `Apache2 license <http://www.apache.org/licenses/LICENSE-2.0.html>`_, so feel free to `contribute <https://github.com/jaimegildesagredo/booby/pulls>`_ and `report errors and suggestions <https://github.com/jaimegildesagredo/booby/issues>`_.

Usage
Expand All @@ -47,13 +49,15 @@ See the sample code below to get an idea of the main features.
login = fields.String(required=True)
name = fields.String()
email = fields.Email()
site = fields.URL()
token = fields.Embedded(Token, required=True)
addresses = fields.Collection(Address)

jack = User(
login='jack',
name='Jack',
email='[email protected]',
url='http://mysite.com',
token={
'key': 'vs7dfxxx',
'secret': 'ds5ds4xxx'
Expand All @@ -73,6 +77,7 @@ See the sample code below to get an idea of the main features.

{
"email": "[email protected]",
"url": "http://mysite.com",
"login": "jack",
"token": {
"secret": "ds5ds4xxx",
Expand All @@ -91,6 +96,70 @@ See the sample code below to get an idea of the main features.
]
}

Advanced
--------

Booby raises when you try to pass a property as input dict thata not exist as property. It's very annoying when you want to pass a big JSON and want that Booby take only their parameters that it understand.

For example:

.. code-block:: python

from booby import Model, fields

class User(Model):
login = fields.String(required=True)
name = fields.String()

input_info = dict(login="john", name="doe", address="other field")

jack = User(**input_info)

This code raise the exception:

.. code-block:: bash

> python example.py
Traceback (most recent call last):
File "/Users/Dani/Projects/apitest/apitest/actions/analyze/console.py", line 39, in launch_analyze_in_console
postman_parser(json_info)
File "/Users/Dani/Projects/apitest/apitest/actions/analyze/postman.py", line 52, in postman_parser
end_points=postman_info.get("item")))
File "/Users/Dani/.virtualenvs/apitest/lib/python3.5/site-packages/booby/models.py", line 108, in __init__
self._update(kwargs)
File "/Users/Dani/.virtualenvs/apitest/lib/python3.5/site-packages/booby/models.py", line 163, in _update
self[k] = v
File "/Users/Dani/.virtualenvs/apitest/lib/python3.5/site-packages/booby/models.py", line 150, in __setitem__
setattr(self, k, v)
File "/Users/Dani/.virtualenvs/apitest/lib/python3.5/site-packages/booby/fields.py", line 317, in __set__
value = self._resolve(value)
File "/Users/Dani/.virtualenvs/apitest/lib/python3.5/site-packages/booby/fields.py", line 325, in _resolve
item = self.model(**item)
File "/Users/Dani/.virtualenvs/apitest/lib/python3.5/site-packages/booby/models.py", line 108, in __init__
self._update(kwargs)
File "/Users/Dani/.virtualenvs/apitest/lib/python3.5/site-packages/booby/models.py", line 163, in _update
self[k] = v
File "/Users/Dani/.virtualenvs/apitest/lib/python3.5/site-packages/booby/models.py", line 148, in __setitem__

booby.errors.FieldError: address

If you want Booby ignore these properties that don't understand, you can define the Model class adding the property `__ignore_missing__`

.. code-block:: python

from booby import Model, fields

class User(Model):
__ignore_missing__ = True

login = fields.String(required=True)
name = fields.String()

input_info = dict(login="john", name="doe", address="other field")

jack = User(**input_info)


Installation
------------

Expand Down
5 changes: 2 additions & 3 deletions booby/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,5 @@
# limitations under the License.


from booby.models import Model

__all__ = ['Model']
from .fields import * # noqa
from .models import * # noqa
87 changes: 79 additions & 8 deletions booby/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,15 @@ class User(Model):
is_active = Boolean(default=False)
"""

import six
import collections

try:
# Python 3 compatiblity
from urlparse import urlparse
except ImportError:
from urllib.parse import urlparse

from booby import (
validators as builtin_validators,
encoders as builtin_encoders,
Expand Down Expand Up @@ -67,17 +74,20 @@ def __init__(self, *validators, **kwargs):
self.options = kwargs

self.default = kwargs.get('default')
self.description = kwargs.get('description', '')
self.required = kwargs.get('required', False)
self.choices = kwargs.get('choices', [])

assert isinstance(self.choices, list)

# Setup field validators
self.validators = []

if kwargs.get('required'):
if self.required:
self.validators.append(builtin_validators.Required())

choices = kwargs.get('choices')

if choices:
self.validators.append(builtin_validators.In(choices))
if self.choices:
self.validators.append(builtin_validators.In(self.choices))

self.validators.extend(validators)

Expand All @@ -100,6 +110,8 @@ def __get__(self, instance, owner):
return self

def __set__(self, instance, value):
if not value:
value = self.default
instance._data[self] = value

def _default(self, model):
Expand Down Expand Up @@ -133,34 +145,58 @@ def encode(self, value):

return value

@property
def field_type(self):
"""
:return: Python native type: int, str, float...
:rtype: object
"""
raise NotImplemented("This property must be implemented by the subclass")


class String(Field):
""":class:`Field` subclass with builtin `string` validation."""

def __init__(self, *args, **kwargs):
super(String, self).__init__(builtin_validators.String(), *args, **kwargs)

@property
def field_type(self):
return six.string_types


class Integer(Field):
""":class:`Field` subclass with builtin `integer` validation."""

def __init__(self, *args, **kwargs):
super(Integer, self).__init__(builtin_validators.Integer(), *args, **kwargs)

@property
def field_type(self):
return six.integer_types


class Float(Field):
""":class:`Field` subclass with builtin `float` validation."""

def __init__(self, *args, **kwargs):
super(Float, self).__init__(builtin_validators.Float(), *args, **kwargs)

@property
def field_type(self):
return float


class Boolean(Field):
""":class:`Field` subclass with builtin `bool` validation."""

def __init__(self, *args, **kwargs):
super(Boolean, self).__init__(builtin_validators.Boolean(), *args, **kwargs)

@property
def field_type(self):
return bool


class Embedded(Field):
""":class:`Field` subclass with builtin embedded :class:`models.Model`
Expand All @@ -183,26 +219,58 @@ def __set__(self, instance, value):
super(Embedded, self).__set__(instance, value)


class Email(Field):
class Email(String):
""":class:`Field` subclass with builtin `email` validation."""

def __init__(self, *args, **kwargs):
super(Email, self).__init__(builtin_validators.Email(), *args, **kwargs)


class URL(String):
""":class:`Field` subclass with builtin `URL` validation."""

def __init__(self, *args, **kwargs):
super(URL, self).__init__(builtin_validators.URL(), *args, **kwargs)


class IP(String):
""":class:`Field` subclass with builtin `ip` validation."""

def __init__(self, *args, **kwargs):
super(IP, self).__init__(builtin_validators.IP(), *args, **kwargs)


class URI(String):
""":class:`Field` subclass with builtin `URI` validation."""

def __init__(self, *args, **kwargs):
super(URI, self).__init__(builtin_validators.URI(), *args, **kwargs)


class Raw(Field):
""":class:`Field` raw input data"""

def __init__(self, *args, **kwargs):
super(Raw, self).__init__(*args, **kwargs)


class List(Field):
""":class:`Field` subclass with builtin `list` validation
and default value.

"""

def __init__(self, *args, **kwargs):
kwargs.setdefault('default', list)
kwargs.setdefault('default', [])
kwargs.setdefault('encoders', []).append(builtin_encoders.List())

super(List, self).__init__(
builtin_validators.List(*kwargs.get('inner_validators', [])),
*args, **kwargs)

@property
def field_type(self):
return list


class Collection(Field):
Expand Down Expand Up @@ -237,7 +305,7 @@ class User(Model):
"""

def __init__(self, model, *args, **kwargs):
kwargs.setdefault('default', list)
kwargs.setdefault('default', [])

kwargs.setdefault('encoders', []).append(builtin_encoders.Collection())
kwargs.setdefault('decoders', []).append(builtin_decoders.Collection(model))
Expand All @@ -257,3 +325,6 @@ def _resolve(self, value):
item = self.model(**item)
result.append(item)
return result

__all__ = ("Field", "String", "Integer", "Float", "Boolean", "Embedded", "Email", "URL", "List", "Collection", "IP",
"Raw", "URI")
45 changes: 42 additions & 3 deletions booby/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,13 @@ class Repo(Model):
'{"owner": {"login": "jaimegildesagredo", "name": "Jaime Gil de Sagredo"}, "name": "Booby"}'
"""

import json
import six
import collections
try:
import ujson as json
except ImportError:
import json


from booby import mixins, fields, errors, _utils

Expand Down Expand Up @@ -67,6 +72,7 @@ def __repr__(cls):
_utils.repr_options(cls._fields))


@six.add_metaclass(ModelMeta)
class Model(mixins.Encoder):
"""The `Model` class. All Booby models should subclass this.

Expand All @@ -90,11 +96,10 @@ class Model(mixins.Encoder):

"""

__metaclass__ = ModelMeta

def __new__(cls, *args, **kwargs):
model = super(Model, cls).__new__(cls)
model._data = {}
model.ignore_missing = bool(getattr(model, "__ignore_missing__", False))

return model

Expand Down Expand Up @@ -172,6 +177,22 @@ def is_valid(self):
else:
return True

@property
def descriptions(self):
"""This property returns a dict of strings with the name of propery as a key
and their description as a value"""

res = {}
for name, field in list(self._fields.items()):
res[name] = field.description
return res

def description(self, key):
try:
return self._fields[key].description
except KeyError:
return None

def validate(self):
"""This method validates the entire `model`. That is, validates
all the :mod:`fields` within this model.
Expand Down Expand Up @@ -227,3 +248,21 @@ def decode(self, raw):
result[name] = value

return result

@classmethod
def properties(cls):
"""Get properties defined in Model without instantiate them"""
if hasattr(cls, "_fields"):
return cls._fields
else:
ret = {}
for k, v in six.iteritems(cls.__dict__):
if k.startswith("_"):
continue

ret[k] = v

return ret

__all__ = ("Model", )

Loading