Skip to content

Commit

Permalink
Rework data proxy and to marshmallow schema export
Browse files Browse the repository at this point in the history
  • Loading branch information
touilleMan committed Sep 4, 2016
1 parent cc02d0f commit 2669f87
Show file tree
Hide file tree
Showing 13 changed files with 229 additions and 123 deletions.
51 changes: 32 additions & 19 deletions examples/flask/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
from flask.ext.babel import Babel, gettext
from bson import ObjectId
from pymongo import MongoClient
import marshmallow

from umongo import Instance, Document, fields, ValidationError, set_gettext
from umongo.marshmallow_bonus_fields import UMongoMarshmallowSchema


app = Flask(__name__)
Expand Down Expand Up @@ -74,12 +76,15 @@ def populate_db():
User(**data).commit()


# dump/update can be passed a custom schema instance to avoid some fields
no_pass_schema = User.Schema(load_only=('password',), dump_only=('password',))

# Create a custom marshmallow schema from User document in order to avoid some fields
class UserNoPassSchema(User.schema.as_marshmallow_schema()):
class Meta:
read_only = ('password',)
load_only = ('password',)
no_pass_schema = UserNoPassSchema()

def dump_user_no_pass(u):
return u.dump(schema=no_pass_schema)
return no_pass_schema.dump(u).data


@app.route('/', methods=['GET'])
Expand All @@ -104,9 +109,13 @@ def _to_objid(data):
return None


def _nick_or_id_lookup(nick_or_id):
return {'$or': [{'nick': nick_or_id}, {'_id': _to_objid(nick_or_id)}]}


@app.route('/users/<nick_or_id>', methods=['GET'])
def get_user(nick_or_id):
user = User.find_one({'$or': [{'nick': nick_or_id}, {'_id': _to_objid(nick_or_id)}]})
user = User.find_one(_nick_or_id_lookup(nick_or_id))
if not user:
abort(404)
return jsonify(dump_user_no_pass(user))
Expand All @@ -117,13 +126,19 @@ def update_user(nick_or_id):
payload = request.get_json()
if payload is None:
abort(400, 'Request body must be json with Content-type: application/json')
user = User.find_one({'$or': [{'nick': nick_or_id}, {'_id': _to_objid(nick_or_id)}]})
user = User.find_one(_nick_or_id_lookup(nick_or_id))
if not user:
abort(404)
# Define a custom schema from the default one to ignore read-only fields
schema = User.Schema(dump_only=('password', 'nick'))
UserUpdateSchema = User.Schema.as_marshmallow_schema(params={
'password': {'dump_only': True},
'nick': {'dump_only': True}
})()
# with `strict`, marshmallow raise ValidationError if something is wrong
schema = UserUpdateSchema(strict=True)
try:
user.update(payload, schema=schema)
data, _ = schema.load(payload)
user.update(data)
user.commit()
except ValidationError as ve:
resp = jsonify(message=ve.args[0])
Expand All @@ -134,7 +149,7 @@ def update_user(nick_or_id):

@app.route('/users/<nick_or_id>', methods=['DELETE'])
def delete_user(nick_or_id):
user = User.find_one({'$or': [{'nick': nick_or_id}, {'_id': _to_objid(nick_or_id)}]})
user = User.find_one(_nick_or_id_lookup(nick_or_id))
if not user:
abort(404)
try:
Expand All @@ -151,20 +166,18 @@ def change_password_user(nick_or_id):
payload = request.get_json()
if payload is None:
abort(400, 'Request body must be json with Content-type: application/json')
user = User.find_one({'$or': [{'nick': nick_or_id}, {'_id': _to_objid(nick_or_id)}]})
user = User.find_one(_nick_or_id_lookup(nick_or_id))
if not user:
abort(404)

# Custom schema with only a required password field
class PasswordSchema(User.Schema):
class Meta:
fields = ('password',)

schema = PasswordSchema(strict=True)
schema.fields['password'].required = True
# Use a field from our document to create a marshmallow schema
# Note that we use `UMongoMarshmallowSchema` to get unknown field
# check on deserialization and skip missing fields instead of returning None
class ChangePasswordSchema(UMongoMarshmallowSchema):
password = User.schema.fields['password'].as_marshmallow_field(params={'required': True})
# with `strict`, marshmallow raise ValidationError if something is wrong
schema = ChangePasswordSchema(strict=True)
try:
# Validate the incoming payload outside of the document to process
# the `required` options
data, _ = schema.load(payload)
user.password = data['password']
user.commit()
Expand Down
79 changes: 55 additions & 24 deletions tests/test_data_proxy.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from marshmallow import ValidationError, missing
import pytest

from umongo.data_proxy import DataProxy
from umongo.data_proxy import data_proxy_factory
from umongo import EmbeddedSchema, fields, EmbeddedDocument, validate, exceptions

from .common import BaseTest
Expand All @@ -12,12 +12,14 @@ class TestDataProxy(BaseTest):
def test_repr(self):

class MySchema(EmbeddedSchema):
field_a = fields.IntField()
field_a = fields.IntField(attribute='mongo_field_a')
field_b = fields.StrField()

d = DataProxy(MySchema(), {'field_a': 1, 'field_b': 'value'})
MyDataProxy = data_proxy_factory('My', MySchema())
d = MyDataProxy({'field_a': 1, 'field_b': 'value'})
assert MyDataProxy.__name__ == 'MyDataProxy'
repr_d = repr(d)
assert repr_d.startswith("<DataProxy(")
assert repr_d.startswith("<MyDataProxy(")
assert "'field_a': 1" in repr_d
assert "'field_b': 'value'" in repr_d

Expand All @@ -27,7 +29,8 @@ class MySchema(EmbeddedSchema):
a = fields.IntField()
b = fields.IntField()

d = DataProxy(MySchema())
MyDataProxy = data_proxy_factory('My', MySchema())
d = MyDataProxy()
d.load({'a': 1, 'b': 2})
assert d.get('a') == 1
d.set('b', 3)
Expand All @@ -44,7 +47,8 @@ class MySchema(EmbeddedSchema):
a = fields.IntField()
b = fields.IntField(attribute='in_mongo_b')

d = DataProxy(MySchema())
MyDataProxy = data_proxy_factory('My', MySchema())
d = MyDataProxy()
d.load({'a': 1, 'b': 2})
assert d.to_mongo() == {'a': 1, 'in_mongo_b': 2}

Expand All @@ -55,7 +59,7 @@ class MySchema(EmbeddedSchema):
assert d.to_mongo(update=True) is None
assert d.to_mongo() == {'a': 4, 'in_mongo_b': 5}

d2 = DataProxy(MySchema(), data={'a': 4, 'b': 5})
d2 = MyDataProxy(data={'a': 4, 'b': 5})
assert d == d2

def test_modify(self):
Expand All @@ -64,7 +68,8 @@ class MySchema(EmbeddedSchema):
a = fields.IntField()
b = fields.IntField(attribute='in_mongo_b')

d = DataProxy(MySchema())
MyDataProxy = data_proxy_factory('My', MySchema())
d = MyDataProxy()
d.load({'a': 1, 'b': 2})
assert d.to_mongo() == {'a': 1, 'in_mongo_b': 2}
assert d.to_mongo(update=True) is None
Expand All @@ -86,7 +91,8 @@ class MySchema(EmbeddedSchema):
a = fields.EmbeddedField(MyEmbedded, instance=self.instance)
b = fields.ListField(fields.IntField)

d = DataProxy(MySchema())
MyDataProxy = data_proxy_factory('My', MySchema())
d = MyDataProxy()
d.load({'a': {'aa': 1}, 'b': [2, 3]})
assert d.to_mongo() == {'a': {'aa': 1}, 'b': [2, 3]}
d.get('a').aa = 4
Expand All @@ -103,7 +109,8 @@ class MySchema(EmbeddedSchema):
a = fields.IntField()
b = fields.IntField(attribute='in_mongo_b')

d = DataProxy(MySchema())
MyDataProxy = data_proxy_factory('My', MySchema())
d = MyDataProxy()
d.load({'a': 1, 'b': 2})
d.set('a', 3)
assert d.to_mongo() == {'a': 3, 'in_mongo_b': 2}
Expand All @@ -123,7 +130,8 @@ class MySchema(EmbeddedSchema):
a = fields.IntField()
b = fields.IntField(attribute='in_mongo_b')

d = DataProxy(MySchema())
MyDataProxy = data_proxy_factory('My', MySchema())
d = MyDataProxy()
d.load({'a': 1, 'b': 2})
d.delete('b')
assert d.to_mongo() == {'a': 1}
Expand All @@ -139,7 +147,8 @@ def test_route_naming(self):
class MySchema(EmbeddedSchema):
in_front = fields.IntField(attribute='in_mongo')

d = DataProxy(MySchema())
MyDataProxy = data_proxy_factory('My', MySchema())
d = MyDataProxy()
with pytest.raises(ValidationError):
d.load({'in_mongo': 42})
d.load({'in_front': 42})
Expand All @@ -157,7 +166,8 @@ def test_from_mongo(self):
class MySchema(EmbeddedSchema):
in_front = fields.IntField(attribute='in_mongo')

d = DataProxy(MySchema())
MyDataProxy = data_proxy_factory('My', MySchema())
d = MyDataProxy()
with pytest.raises(KeyError):
d.from_mongo({'in_front': 42})
d.from_mongo({'in_mongo': 42})
Expand All @@ -169,21 +179,38 @@ class MySchema(EmbeddedSchema):
a = fields.IntField()
b = fields.IntField(attribute='in_mongo_b')

d1 = DataProxy(MySchema())
MyDataProxy = data_proxy_factory('My', MySchema())
d1 = MyDataProxy()
d1.load({'a': 1, 'b': 2})
assert d1 == {'a': 1, 'in_mongo_b': 2}

d2 = DataProxy(MySchema())
d2 = MyDataProxy()
d2.load({'a': 1, 'b': 2})
assert d1 == d2

def test_share_ressources(self):

class MySchema(EmbeddedSchema):
a = fields.IntField()
b = fields.IntField(attribute='in_mongo_b')

MyDataProxy = data_proxy_factory('My', MySchema())
d1 = MyDataProxy()
d2 = MyDataProxy()
for field in ('schema', '_fields', '_fields_from_mongo_key'):
assert getattr(d1, field) is getattr(d2, field)
d1.load({'a': 1})
d2.load({'b': 2})
assert d1 != d2

def test_access_by_mongo_name(self):

class MySchema(EmbeddedSchema):
a = fields.IntField()
b = fields.IntField(attribute='in_mongo_b')

d = DataProxy(MySchema(), data={'a': 1, 'b': 2})
MyDataProxy = data_proxy_factory('My', MySchema())
d = MyDataProxy(data={'a': 1, 'b': 2})
assert d.get_by_mongo_name('in_mongo_b') == 2
assert d.get_by_mongo_name('a') == 1
with pytest.raises(KeyError):
Expand All @@ -200,7 +227,8 @@ class MySchema(EmbeddedSchema):
a = fields.IntField()
b = fields.IntField(attribute='in_mongo_b')

d = DataProxy(MySchema(), data={'a': 1})
MyDataProxy = data_proxy_factory('My', MySchema())
d = MyDataProxy(data={'a': 1})
assert d.get('b') is missing
assert d.get_by_mongo_name('in_mongo_b') is missing
assert d._data['in_mongo_b'] is missing
Expand All @@ -217,7 +245,8 @@ class MySchema(EmbeddedSchema):
with_default = fields.StrField(default='default_value')
with_missing = fields.StrField(missing='missing_value')

d = DataProxy(MySchema(), data={})
MyDataProxy = data_proxy_factory('My', MySchema())
d = MyDataProxy(data={})
assert d._data['with_default'] is missing
assert d._data['with_missing'] is 'missing_value'
assert d.get('with_default') == 'default_value'
Expand All @@ -230,9 +259,10 @@ def test_validate(self):
class MySchema(EmbeddedSchema):
with_max = fields.IntField(validate=validate.Range(max=99))

d = DataProxy(MySchema(), data={})
MyDataProxy = data_proxy_factory('My', MySchema())
d = MyDataProxy(data={})
with pytest.raises(ValidationError) as exc:
DataProxy(MySchema(), data={'with_max': 100})
MyDataProxy(data={'with_max': 100})
assert exc.value.args[0] == {'with_max': ['Must be at most 99.']}
with pytest.raises(ValidationError) as exc:
d.set('with_max', 100)
Expand All @@ -247,7 +277,8 @@ class MySchema(EmbeddedSchema):
loaded = fields.StrField()
loaded_but_empty = fields.StrField()

d = DataProxy(MySchema())
MyDataProxy = data_proxy_factory('My', MySchema())
d = MyDataProxy()
d.from_mongo({'loaded': "foo", 'loaded_but_empty': missing}, partial=True)
assert d.partial is True
for field in ('with_default', 'with_missing', 'normal'):
Expand All @@ -266,7 +297,7 @@ class MySchema(EmbeddedSchema):
assert d.get('loaded') is missing

# Same test, but using `load`
d = DataProxy(MySchema())
d = MyDataProxy()
d.load({'loaded': "foo", 'loaded_but_empty': missing}, partial=True)
assert d.partial is True
for field in ('with_default', 'with_missing', 'normal'):
Expand All @@ -285,7 +316,7 @@ class MySchema(EmbeddedSchema):
assert d.get('loaded') is missing

# Not partial
d = DataProxy(MySchema())
d = MyDataProxy()
d.from_mongo({'loaded': "foo", 'loaded_but_empty': missing})
assert d.partial is False
assert d.get('with_default') == 'default_value'
Expand All @@ -294,7 +325,7 @@ class MySchema(EmbeddedSchema):
assert d.get('loaded') == "foo"
assert d.get('loaded_but_empty') == missing
# Same test with load
d = DataProxy(MySchema())
d = MyDataProxy()
d.load({'loaded': "foo", 'loaded_but_empty': missing})
assert d.partial is False
assert d.partial is False
Expand Down

0 comments on commit 2669f87

Please sign in to comment.