Skip to content

Commit

Permalink
Merge 1283a6e into db93916
Browse files Browse the repository at this point in the history
  • Loading branch information
jfinkels committed Feb 26, 2016
2 parents db93916 + 1283a6e commit 895c6de
Show file tree
Hide file tree
Showing 13 changed files with 871 additions and 500 deletions.
3 changes: 3 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ Not yet released.
- #449: roll back the session on any SQLAlchemy error, not just a few.
- #436, #453: use ``__table__.name`` instead of ``__tablename__`` to infer the
collection name for the SQLAlchemy model.
- #440, #475: uses the serialization function provided at the time of invoking
:meth:`APIManager.create_api` to serialize each resource correctly, depending
on its type.
- #474: include license files in built wheel for distribution.

Older versions
Expand Down
2 changes: 2 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ in Flask-Restless.

.. autofunction:: model_for(collection_name, _apimanager=None)

.. autofunction:: serializer_for(model, _apimanager=None)

.. autofunction:: url_for(model, instid=None, relationname=None, relationinstid=None, _apimanager=None, **kw)

.. autoclass:: ProcessingException
25 changes: 24 additions & 1 deletion docs/customizing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -195,11 +195,34 @@ follows::

For a complete version of this example, see the
:file:`examples/server_configurations/custom_serialization.py` module in the
source distribution, or `view it online`_
source distribution, or `view it online`_.

.. _Marshmallow: https://marshmallow.readthedocs.org
.. _view it online: https://github.com/jfinkels/flask-restless/tree/master/examples/server_configurations/custom_serialization.py

Per-model serialization
-----------------------

The correct serialization function will be used for each type of SQLAlchemy
model for which you invoke :meth:`APIManager.create_api`. For example, if you
create two APIs, one for ``Person`` objects and one for ``Article`` objects, ::

manager.create_api(Person, serializer=person_serializer)
manager.create_api(Article, serializer=article_serializer)

and then make a request like

.. sourcecode:: http

GET /api/article/1?include=author HTTP/1.1
Host: example.com
Accept: application/vnd.api+json

then Flask-Restless will use the ``article_serializer`` function to serialize
the primary data (that is, the top-level ``data`` element in the response
document) and the ``person_serializer`` to serialize the included ``Person``
resource.

.. _validation:

Capturing validation errors
Expand Down
1 change: 1 addition & 0 deletions flask_restless/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
# ``from flask.ext.restless import APIManager``, for example.
from .helpers import collection_name
from .helpers import model_for
from .helpers import serializer_for
from .helpers import url_for
from .manager import APIManager
from .manager import IllegalArgumentError
Expand Down
55 changes: 53 additions & 2 deletions flask_restless/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,27 @@ def __call__(self, model, resource_id=None, relation_name=None,
raise ValueError(message)


class SerializerFinder(KnowsAPIManagers, Singleton):
"""The singleton class that backs the :func:`serializer_for` function."""

def __call__(self, model, _apimanager=None, **kw):
if _apimanager is not None:
if model not in _apimanager.created_apis_for:
message = ('APIManager {0} has not created an API for model '
' {1}').format(_apimanager, model)
raise ValueError(message)
return _apimanager.serializer_for(model, **kw)
for manager in self.created_managers:
try:
return self(model, _apimanager=manager, **kw)
except ValueError:
pass
message = ('Model {0} is not known to any APIManager'
' objects; maybe you have not called'
' APIManager.create_api() for this model.').format(model)
raise ValueError(message)


#: Returns the URL for the specified model, similar to :func:`flask.url_for`.
#:
#: `model` is a SQLAlchemy model class. This should be a model on which
Expand All @@ -458,8 +479,8 @@ def __call__(self, model, resource_id=None, relation_name=None,
#: :class:`APIManager`. Restrict our search for endpoints exposing `model` to
#: only endpoints created by the specified :class:`APIManager` instance.
#:
#: `resource_id`, `relation_name`, and `relationresource_id` allow you to get a more
#: specific sub-resource.
#: The `resource_id`, `relation_name`, and `relationresource_id` keyword
#: arguments allow you to get the URL for a more specific sub-resource.
#:
#: For example, suppose you have a model class ``Person`` and have created the
#: appropriate Flask application and SQLAlchemy session::
Expand Down Expand Up @@ -521,6 +542,36 @@ def __call__(self, model, resource_id=None, relation_name=None,
#:
collection_name = CollectionNameFinder()

#: Returns the callable serializer object for the specified model, as
#: specified by the `serializer` keyword argument to
#: :meth:`APIManager.create_api` when it was previously invoked on the
#: model.
#:
#: `model` is a SQLAlchemy model class. This should be a model on which
#: :meth:`APIManager.create_api_blueprint` (or :meth:`APIManager.create_api`)
#: has been invoked previously. If no API has been created for it, this
#: function raises a `ValueError`.
#:
#: If `_apimanager` is not ``None``, it must be an instance of
#: :class:`APIManager`. Restrict our search for endpoints exposing
#: `model` to only endpoints created by the specified
#: :class:`APIManager` instance.
#:
#: For example, suppose you have a model class ``Person`` and have
#: created the appropriate Flask application and SQLAlchemy session::
#:
#: >>> from mymodels import Person
#: >>> def my_serializer(model, *args, **kw):
#: ... # return something cool here...
#: ... return {}
#: ...
#: >>> manager = APIManager(app, session=session)
#: >>> manager.create_api(Person, serializer=my_serializer)
#: >>> serializer_for(Person)
#: <function my_serializer at 0x...>
#:
serializer_for = SerializerFinder()

#: Returns the model corresponding to the given collection name, as specified
#: by the ``collection_name`` keyword argument to :meth:`APIManager.create_api`
#: when it was previously invoked on the model.
Expand Down
40 changes: 32 additions & 8 deletions flask_restless/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from flask import Blueprint

from .helpers import collection_name
from .helpers import serializer_for
from .helpers import model_for
from .helpers import url_for
from .serialization import DefaultSerializer
Expand Down Expand Up @@ -57,16 +58,21 @@
#:
#: These tuples are used by :class:`APIManager` to store information about
#: Flask applications registered using :meth:`APIManager.init_app`.
RestlessInfo = namedtuple('RestlessInfo', ['session',
'universal_preprocessors',
'universal_postprocessors'])
# RestlessInfo = namedtuple('RestlessInfo', ['session',
# 'universal_preprocessors',
# 'universal_postprocessors'])

#: A tuple that stores information about a created API.
#:
#: The first element, `collection_name`, is the name by which a collection of
#: instances of the model which this API exposes is known. The second element,
#: `blueprint_name`, is the name of the blueprint that contains this API.
APIInfo = namedtuple('APIInfo', 'collection_name blueprint_name')
#: The elements are, in order,
#:
#: - `collection_name`, the name by which a collection of instances of
#: the model exposed by this API is known,
#: - `blueprint_name`, the name of the blueprint that contains this API,
#: - `serializer`, the subclass of :class:`Serializer` provided for the
#: model exposed by this API.
#:
APIInfo = namedtuple('APIInfo', 'collection_name blueprint_name serializer')


class IllegalArgumentError(Exception):
Expand Down Expand Up @@ -163,6 +169,7 @@ def __init__(self, app=None, session=None, flask_sqlalchemy_db=None,
url_for.register(self)
model_for.register(self)
collection_name.register(self)
serializer_for.register(self)

#: A mapping whose keys are models for which this object has
#: created an API via the :meth:`create_api_blueprint` method
Expand Down Expand Up @@ -298,7 +305,23 @@ def collection_name(self, model):
"""
return self.created_apis_for[model].collection_name

def serializer_for(self, model):
"""Returns the serializer for the specified model, as specified
by the `serializer` keyword argument to
:meth:`create_api_blueprint`.
`model` is a SQLAlchemy model class. This must be a model on
which :meth:`create_api_blueprint` has been invoked previously,
otherwise a :exc:`KeyError` is raised.
This method only returns URLs for endpoints created by this
:class:`APIManager`.
"""
return self.created_apis_for[model].serializer

def init_app(self, app):

"""Registers any created APIs on the given Flask application.
This function should only be called if no Flask application was
Expand Down Expand Up @@ -734,7 +757,8 @@ def create_api_blueprint(self, name, model, methods=READONLY_METHODS,

# Finally, record that this APIManager instance has created an API for
# the specified model.
self.created_apis_for[model] = APIInfo(collection_name, blueprint.name)
self.created_apis_for[model] = APIInfo(collection_name, blueprint.name,
serializer)
return blueprint

def create_api(self, *args, **kw):
Expand Down
35 changes: 21 additions & 14 deletions flask_restless/serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
from .helpers import is_like_list
from .helpers import primary_key_name
from .helpers import primary_key_value
from .helpers import serializer_for
from .helpers import strings_to_datetimes
from .helpers import url_for

Expand All @@ -73,17 +74,24 @@ class SerializationException(Exception):
"""Raised when there is a problem serializing an instance of a
SQLAlchemy model to a dictionary representation.
``instance`` is the (problematic) instance on which
`instance` is the (problematic) instance on which
:meth:`Serializer.__call__` was invoked.
`message` is an optional string describing the problem in more
detail.
`resource` is an optional partially-constructed serialized
representation of ``instance``.
Each of these keyword arguments is stored in a corresponding
instance attribute so client code can access them.
"""

def __init__(self, instance, resource=None, *args, **kw):
def __init__(self, instance, message=None, resource=None, *args, **kw):
super(SerializationException, self).__init__(*args, **kw)
self.resource = resource
self.message = message
self.instance = instance


Expand Down Expand Up @@ -533,12 +541,20 @@ def __call__(self, instance, only=None):
# attributes. This may happen if, for example, the return value
# of one of the callable functions is an instance of another
# SQLAlchemy model class.
for k, v in attributes.items():
for key, val in attributes.items():
# This is a bit of a fragile test for whether the object
# needs to be serialized: we simply check if the class of
# the object is a mapped class.
if is_mapped_class(type(v)):
attributes[k] = simple_serialize(v)
if is_mapped_class(type(val)):
model_ = get_model(val)
try:
serialize = serializer_for(model_)
except ValueError:
# TODO Should this cause an exception, or fail
# silently? See similar comments in `views/base.py`.
# # raise SerializationException(instance)
serialize = simple_serialize
attributes[key] = serialize(val)
# Get the ID and type of the resource.
id_ = attributes.pop('id')
type_ = collection_name(model)
Expand Down Expand Up @@ -575,15 +591,6 @@ def __call__(self, instance, only=None):
# value = value()
# result[method] = value

# Recursively serialize values that are themselves SQLAlchemy
# models.
#
# TODO We really need to serialize each model using the
# serializer defined for that class when the user called
# APIManager.create_api
for key, value in result.items():
if key not in column_attrs and is_mapped_class(type(value)):
result[key] = simple_serialize(value)
# If the primary key is not named "id", we'll duplicate the
# primary key under the "id" key.
pk_name = primary_key_name(model)
Expand Down
Loading

0 comments on commit 895c6de

Please sign in to comment.