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

Improvement/performance tweaks [WIP] #54

Open
wants to merge 4 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
158 changes: 130 additions & 28 deletions docs/guide/serializers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -110,25 +110,21 @@ All field classes accept this set of arguments:
intead of relying on param labels.*

* **source** *(str, optional):* name of internal object key/attribute
that will be passed to field's on ``.to_representation(value)`` call.
Special ``'*'`` value is allowed that will pass whole object to
field when making representation. If not set then default source will
be a field name used as a serializer's attribute.
that will be passed to field's on ``.to_representation(value)`` call. If not
set then default source is a field name used as a serializer's attribute.

* **validators** *(list, optional):* list of validator callables.

* **many** *(bool, optional)* set to True if field is in fact a list
of given type objects


.. note::

``source='*'`` is in fact a dirty workaround and will not work well
on validation when new object instances needs to be created/updated
using POST/PUT requests. This works quite well with simple retrieve/list
type resources but in more sophisticated cases it is better to use
custom object properties as sources to encapsulate such fields.
.. versionchanged:: 1.0.0

Fields no no longer have special case treatment for ``source='*'`` argument.
If you want to access multiple object keys and values within single
serializer field please refer to :ref:`guide-field-attribute-access` section
of this document.

.. _field-validation:

Expand All @@ -143,9 +139,9 @@ in order to provide correct HTTP responses each validator shoud raise
.. note::

Concept of validation for fields is understood here as a process of checking
if data of valid type (successfully parsed/processed by
``.from_representation`` handler) does meet some other constraints
(lenght, bounds, unique, etc).
if data of valid type (i.e. data that was successfully parsed/processed by
``.from_representation()`` handler) does meet some other constraints
(lenght, bounds, uniquess, etc).


Example of simple validator usage:
Expand Down Expand Up @@ -174,31 +170,50 @@ Resource validation
~~~~~~~~~~~~~~~~~~~

In most cases field level validation is all that you need but sometimes you
need to perfom obejct level validation that needs to access multiple fields
that are already deserialized and validated. Suggested way to do this in
graceful is to override serializer's ``.validate()`` method and raise
:class:`graceful.errors.ValidationError` when your validation fails. This
exception will be then automatically translated to HTTP Bad Request response
on resource-level handlers. Here is example:
need to perfom validation on whole resource representation or deserialized
object. It is possible to access multiple fields that were already deserialized
and pre-validated directly from serializer class.

You can provide your own object-level serialization handler using serializer's
``validate()`` method. This method accepts two arguments:

* **object_dict** *(dict):* it is deserialized object dictionary that already
passed validation. Field sources instead of their representation names are
used as its keys.

* **partial** *(bool):* it is set to ``True`` only on partial object updates
(e.g. on ``PATCH`` requests). If you plan to support partial resource
modification you should check this field and verify if you object has
all the existing keys.

If your validation fails you should raise the
:class:`graceful.errors.ValidationError` exception. Following is the example
of resource serializer with custom object-level validation:


.. code-block:: python

class DrinkSerializer():
alcohol = StringField("main ingredient", required=True)
mixed_with = StringField("what makes it tasty", required=True)

def validate(self, object_dict, partial=False):
# note: always make sure to call super `validate()`
# so whole validation of fields works as expected
super().validate(object_dict, partial)
alcohol = StringField("main ingredient")
mixed_with = StringField("what makes it tasty")

def validate(self, object_dict, partial):
if partial and any([
'alcohol' in object_dict,
'mixed_with' in object_dict,
]):
raise ValidationError(
"bartender refused to change ingredients"
)

# here is a place for your own validation
if (
object_dict['alcohol'] == 'whisky' and
object_dict['mixed_with'] == 'cola'
):
raise ValidationError("bartender refused!')
raise ValidationError(
"bartender refused to mix whisky with cola!"
)


Custom fields
Expand Down Expand Up @@ -228,3 +243,90 @@ as a serialized JSON string that we would like to (de)serialize:
def to_representation(data):
return json.loads(data)

.. _guide-field-attribute-access:


Accessing multiple fields at once
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Sometimes you need to access multiple fields of internal object instance at
once in order to properly represent data in your API. This is very common when
interacting with legacy services/components that cannot be changed or when
your storage engine simply does not allow to store nested or structured objects.

Serializers generally work on per-field basis and allow only to translate field
names between representation and application internal objects. In order to
manipulate multiple representation or internal object instance keys within the
single field you need to create custom field class and override one or more
of following methods:

* ``read_instance(self, instance, source)``: read value from the
object instance before serialization. The return value will be later passed
as an argument to ``to_representation()`` method. The ``source`` argument is
field's name or configured source name (if custom ``source`` is explicitly
specified for that field). Base implementation defaults to dictionary key
lookup or object attribute lookup.

* ``update_instance(self, instance, source, value)``: update the
content of object instance after deserialization. The ``value`` argument is
the return value of ``from_representation()`` method. The ``source`` argument
is the field's name or source name (if custom ``source`` is explicitly
specified for that field). Base implementation defaults to dictionary key
assignment or object attribute assignment.

* ``read_representation(self, representation, field_name)``: read value
from the representation before deserialization. The return value will be
later passed as an argument to ``from_representation()`` method. The
``field_name`` argument is a field's name. Base implementation defaults
to dictionary key lookup or object attribute lookup.

* ``update_representation(self, representation, field_name, value)``:
update the content of representation instance after serialization.
The ``value`` argument is the return value of ``to_representation()`` method.
The ``field_name`` argument is a field's name. Base implementation
defaults to dictionary key assignment.

To better explain how to use these methods let's assume that due to some
storage backend constraints we cannot save nested dictionaries. All of fields
of some nested object will have to be stored under separate keys but we still
want to present this to the user as separate nested dictionary. And of course
we want to support both writes and saves.

.. code-block:: python

class OwnerField(RawField):
def from_representation(self, data):
if not isinstance(data, dict):
raise ValueError("expected object")

return {
'owner_name': data.get('name'),
'owner_age': data.get('age'),
}

def to_representation(self, value):
return {
'age': value.get('owner_age'),
'name': value.get('owner_name'),
}

def validate(self, value):
if 'owner_age' not in value or not isinstance(value['owner_age'], int):
raise ValidationError("invalid owner age")

if 'owner_name' not in value:
raise ValidationError("invalid owner name")

def update_instance(self, instance, source, value):
# we assume that instance is always a dictionary so we can
# use the .update() method
instance.update(value)

def read_instance(self, instance, source):
# .to_representation() method requires acces to whole object
# dictionary so we have to return whole object.
return instance


Similar approach may be used to flatten nested objects into more compact
representations.
2 changes: 1 addition & 1 deletion src/graceful/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def _get_description(self):
"forbidden: {}".format(self.forbidden)
if self.forbidden else ""
),
"invalid: {}:".format(self.invalid) if self.invalid else "",
"invalid: {}".format(self.invalid) if self.invalid else "",
(
"failed to parse: {}".format(self.failed)
if self.failed else ""
Expand Down
67 changes: 67 additions & 0 deletions src/graceful/fields.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import inspect
from collections import Mapping, MutableMapping

from graceful.validators import min_validator, max_validator

Expand Down Expand Up @@ -60,6 +61,14 @@ def from_representation(self, data):
def to_representation(self, value):
return ["True", "False"][value]

.. versionchanged:: 1.0.0
Field instances no longer support ``source="*"`` to access whole object
for the purpose of representation serialization/deserialization. If you
want to access multiple fields of object instance and/or its
representation you must override ``read_*`` and ``update_*`` methods in
your custom field classes (see :ref:`guide-field-attribute-access`
section in documentation).

"""

#: Two-tuple ``(label, url)`` pointing to represented type specification
Expand Down Expand Up @@ -172,6 +181,64 @@ def validate(self, value):
for validator in self.validators:
validator(value)

def update_instance(self, instance, source, value):
"""Update object instance after deserialization.

Args:
instance (object): dictionary or object after serialization.
source (str): field's name or source if ``source`` was
explicitly specified for that field.
value (object): return value from ``from_representation`` method.
"""
if isinstance(instance, MutableMapping):
instance[source] = value
else:
setattr(instance, source, value)

def read_instance(self, instance, source):
"""Read value from the object instance before serialization.

Args:
instance (object): dictionary or object before serialization.
source (str): field's name or source if ``source`` was
explicitly specified for that field.

Returns:
The value that will be later passed as an argument to
``to_representation()`` method.
"""
if isinstance(instance, Mapping):
return instance.get(source, None)

return getattr(instance, source, None)

def update_representation(self, representation, field_name, value):
"""Update representation after field serialization.

Args:
representation (dict): representation dictionary.
attribute_or_key (str): field's name.
value (object): return value from ``to_representation`` method.
"""
# note: Unlike instances, representations can only be dictionaries
# and their type cannot be overriden inside of serializer's class
representation[field_name] = value

def read_representation(self, representation, field_name):
"""Read value from the representation before deserialization.

Args:
representation (dict): representation dictionary.
field_name (str): field's name.

Returns:
The value that will be later passed as an argument to
``from_representation()`` method.
"""
# note: Unlike instances, representations can only be dictionaries
# and their type cannot be overriden inside of serializer's class
return representation.get(field_name, None)


class RawField(BaseField):
"""Represents raw field subtype.
Expand Down
8 changes: 4 additions & 4 deletions src/graceful/resources/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,11 +424,11 @@ def require_validated(self, req, partial=False, bulk=False):

try:
for representation in representations:
object_dict = self.serializer.from_representation(
representation
object_dicts.append(
self.serializer.from_representation(
representation, partial
)
)
self.serializer.validate(object_dict, partial)
object_dicts.append(object_dict)

except DeserializationError as err:
# when working on Resource we know that we can finally raise
Expand Down
Loading