diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index be3b7b0..9078349 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -6,6 +6,7 @@ Changelog
Features:
+- Add Flask-SQLAlchemy/marshmallow-sqlalchemy support via the ``ModelSchema`` class.
- ``Schema.jsonify`` now takes the same arguments as ``marshmallow.Schema.dump``. Additional keyword arguments are passed to ``flask.jsonify``.
diff --git a/README.rst b/README.rst
index 9d30b16..4686c35 100644
--- a/README.rst
+++ b/README.rst
@@ -13,7 +13,13 @@ Flask-Marshmallow
Flask + marshmallow for beautiful APIs
======================================
-Flask-Marshmallow is a thin integration layer for `Flask`_ (a Python web framework) and `marshmallow`_ (an object serialization/deserialization library) that adds additional features to marshmallow, including URL and Hyperlinks fields for HATEOAS-ready APIs.
+Flask-Marshmallow is a thin integration layer for `Flask`_ (a Python web framework) and `marshmallow`_ (an object serialization/deserialization library) that adds additional features to marshmallow, including URL and Hyperlinks fields for HATEOAS-ready APIs. It also (optionally) integrates with `Flask-SQLAlchemy `_.
+
+Get it now
+----------
+::
+
+ pip install flask-marshmallow
Create your app.
@@ -53,9 +59,6 @@ Define your output format with marshmallow.
'collection': ma.URLFor('authors')
})
- user_schema = UserSchema()
- users_schema = UserSchema(many=True)
-
Output the data in your views.
@@ -66,12 +69,13 @@ Output the data in your views.
all_users = User.all()
result = users_schema.dump(all_users)
return jsonify(result.data)
+ # OR
+ # return user_schema.jsonify(all_users)
@app.route('/api/users/')
def user_detail(id):
user = User.get(id)
- result = user_schema.dump(user)
- return jsonify(result.data)
+ return user_schema.jsonify(user)
# {
# "email": "fred@queen.com",
# "date_created": "Fri, 25 Apr 2014 06:02:56 -0000",
@@ -82,22 +86,15 @@ Output the data in your views.
# }
+http://flask-marshmallow.readthedocs.org/
+=========================================
+
Learn More
==========
To learn more about marshmallow, check out its `docs `_.
-Get it now
-==========
-
-::
-
- pip install flask-marshmallow
-
-
-http://flask-marshmallow.readthedocs.org/
-=========================================
Project Links
=============
diff --git a/dev-requirements.txt b/dev-requirements.txt
index 10a7b66..68083ea 100644
--- a/dev-requirements.txt
+++ b/dev-requirements.txt
@@ -6,3 +6,7 @@ mock
# Syntax checking
flake8==2.4.0
+
+# Soft requirements
+flask-sqlalchemy
+marshmallow-sqlalchemy
diff --git a/docs/conf.py b/docs/conf.py
index 63f3944..5deb4f6 100755
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -5,7 +5,19 @@
sys.path.insert(0, os.path.abspath('..'))
import flask_marshmallow
sys.path.append(os.path.abspath("_themes"))
-extensions = ['sphinx.ext.autodoc', 'sphinx_issues']
+extensions = [
+ 'sphinx.ext.autodoc',
+ 'sphinx.ext.intersphinx',
+ 'sphinx_issues'
+]
+
+intersphinx_mapping = {
+ 'python': ('http://python.readthedocs.org/en/latest/', None),
+ 'flask': ('http://flask.pocoo.org/docs/latest/', None),
+ 'flask-sqlalchemy': ('http://flask-sqlalchemy.pocoo.org/latest/', None),
+ 'marshmallow': ('http://marshmallow.readthedocs.org/en/latest/', None),
+ 'marshmallow-sqlalchemy': ('http://marshmallow-sqlalchemy.readthedocs.org/en/latest/', None),
+}
primary_domain = 'py'
default_role = 'py:obj'
diff --git a/docs/index.rst b/docs/index.rst
index 6d0f4e5..99a0445 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -11,7 +11,13 @@ Flask-Marshmallow: Flask + marshmallow for beautiful APIs
Flask + marshmallow for beautiful APIs
======================================
-Flask-Marshmallow is a thin integration layer for `Flask`_ (a Python web framework) and `marshmallow`_ (an object serialization/deserialization library) that adds additional features to marshmallow, including URL and Hyperlinks fields for HATEOAS-ready APIs.
+Flask-Marshmallow is a thin integration layer for `Flask`_ (a Python web framework) and `marshmallow`_ (an object serialization/deserialization library) that adds additional features to marshmallow, including URL and Hyperlinks fields for HATEOAS-ready APIs. It also (optionally) integrates with `Flask-SQLAlchemy `_.
+
+Get it now
+----------
+::
+
+ pip install flask-marshmallow
Create your app.
@@ -78,30 +84,99 @@ Output the data in your views.
# }
-Learn More
-==========
-To learn more about marshmallow, check out its `docs `_.
+Optional Flask-SQLAlchemy Integration
+-------------------------------------
+Flask-Marshmallow includes useful extras for integrating with `Flask-SQLAlchemy `_ and `marshmallow-sqlalchemy `_.
-Get it now
-==========
+To enable SQLAlchemy integration, make sure that both Flask-SQLAlchemy and marshmallow-sqlalchemy are installed. ::
-::
+ pip install -U flask-sqlalchemy marshmallow-sqlalchemy
- pip install flask-marshmallow
+Next, initialize the `SQLAlchemy ` and `Marshmallow ` extensions, in that order.
+
+.. code-block:: python
+
+ from flask import Flask
+ from flask_sqlalchemy import SQLAlchemy
+ from flask_marshmallow import Marshmallow
+
+ app = Flask(__name__)
+ app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
+
+ db = SQLAlchemy(app)
+ ma = Marshmallow(app)
+
+.. warning::
+
+ Flask-SQLAlchemy **must** be initialized before Flask-Marshmallow.
+
+
+Declare your models like normal.
+
+
+.. code-block:: python
+
+ class Author(db.Model):
+ id = db.Column(db.Integer, primary_key=True)
+ name = db.Column(db.String(255))
+
+ class Book(db.Model):
+ id = db.Column(db.Integer, primary_key=True)
+ title = db.Column(db.String(255))
+ author_id = db.Column(db.Integer, db.ForeignKey('author.id'))
+ author = db.relationship('Author', backref='books')
+
+
+Generate marshmallow `Schemas ` from your models using `ModelSchema `.
+
+.. code-block:: python
+
+ class AuthorSchema(ma.ModelSchema):
+ class Meta:
+ model = Author
+
+ class BookSchema(ma.ModelSchema):
+ class Meta:
+ model = Book
+
+You can now use your schema to dump and load your ORM objects.
+
+
+.. code-block:: python
+
+ >>> db.create_all()
+ >>> author_schema = AuthorSchema()
+ >>> book_schema = BookSchema()
+ >>> author = Author(name='Chuck Paluhniuk')
+ >>> book = Book(title='Fight Club', author=author)
+ >>> db.session.add(author)
+ >>> db.session.add(book)
+ >>> db.session.commit()
+ >>> author_schema.dump(author).data
+ {'id': 1, 'name': 'Chuck Paluhniuk', 'books': [1]}
+
+
+`ModelSchema ` is nearly identical in API to `marshmallow_sqlalchemy.ModelSchema` with the following exceptions:
+
+- `ModelSchema ` uses the scoped session created by Flask-SQLAlchemy.
+- `ModelSchema ` subclasses `flask_marshmallow.Schema`, so it includes the `jsonify ` method.
API
===
.. automodule:: flask_marshmallow
- :inherited-members:
+ :members:
.. automodule:: flask_marshmallow.fields
:members:
+.. automodule:: flask_marshmallow.sqla
+ :members:
+
Useful Links
============
diff --git a/flask_marshmallow/__init__.py b/flask_marshmallow/__init__.py
index 3b191af..981c519 100755
--- a/flask_marshmallow/__init__.py
+++ b/flask_marshmallow/__init__.py
@@ -10,14 +10,21 @@
:license: MIT, see LICENSE for more details.
"""
-from flask import jsonify
from marshmallow import (
- Schema as BaseSchema,
fields as base_fields,
exceptions,
pprint
)
from . import fields
+from .schema import Schema
+
+try:
+ import flask_sqlalchemy # flake8: noqa
+ from . import sqla
+except ImportError:
+ has_sqla = False
+else:
+ has_sqla = True
__version__ = '0.6.0.dev'
__author__ = 'Steven Loria'
@@ -45,32 +52,13 @@ def _attach_fields(obj):
setattr(obj, attr, getattr(fields, attr))
-class Schema(BaseSchema):
- """Base serializer with which to define custom serializers.
-
- http://marshmallow.readthedocs.org/en/latest/api_reference.html#serializer
- """
-
- def jsonify(self, obj, many=False, *args, **kwargs):
- """Return a JSON response containing the serialized data.
-
-
- :param obj: Object to serialize.
- :param bool many: Set to `True` if `obj` should be serialized as a collection.
- :param kwargs: Additional keyword arguments passed to `flask.jsonify`.
-
- .. versionchanged:: 0.6.0
- Takes the same arguments as `marshmallow.Schema.dump`. Additional
- keyword arguments are passed to `flask.jsonify`.
- """
- data = self.dump(obj, many=many).data
- return jsonify(data, *args, **kwargs)
-
class Marshmallow(object):
"""Wrapper class that integrates Marshmallow with a Flask application.
To use it, instantiate with an application::
+ from flask import Flask
+
app = Flask(__name__)
ma = Marshmallow(app)
@@ -91,15 +79,30 @@ class Meta:
'collection': ma.URLFor('book_list')
})
+
+ In order to integrate with Flask-SQLAlchemy, this extension must by initialized *after*
+ `flask_sqlalchemy.SQLAlchemy`. ::
+
+ db = SQLAlchemy(app)
+ ma = Marshmallow(app)
+
+ This gives you access to `ma.ModelSchema`, which generates a marshmallow
+ `Schema ` based on the passed in model. ::
+
+ class AuthorSchema(ma.ModelSchema):
+ class Meta:
+ model = Author
+
:param Flask app: The Flask application object.
"""
def __init__(self, app=None):
- if app is not None:
- self.init_app(app)
-
self.Schema = Schema
+ if has_sqla:
+ self.ModelSchema = sqla.ModelSchema
_attach_fields(self)
+ if app is not None:
+ self.init_app(app)
def init_app(self, app):
"""Initializes the application with the extension.
@@ -107,4 +110,9 @@ def init_app(self, app):
:param Flask app: The Flask application object.
"""
app.extensions = getattr(app, 'extensions', {})
+
+ # If using Flask-SQLAlchemy, attach db.session to ModelSchema
+ if has_sqla and 'sqlalchemy' in app.extensions:
+ db = app.extensions['sqlalchemy'].db
+ self.ModelSchema.OPTIONS_CLASS.session = db.session
app.extensions[EXTENSION_NAME] = self
diff --git a/flask_marshmallow/fields.py b/flask_marshmallow/fields.py
index 577d364..999a6f3 100755
--- a/flask_marshmallow/fields.py
+++ b/flask_marshmallow/fields.py
@@ -7,7 +7,7 @@
Custom, Flask-specific fields. See the following link for a list of all available
fields from the marshmallow library.
- See http://marshmallow.readthedocs.org/en/latest/api_reference.html#module-marshmallow.fields
+ See the `marshmallow.fields` module for information about the available fields.
"""
import re
import sys
diff --git a/flask_marshmallow/schema.py b/flask_marshmallow/schema.py
new file mode 100644
index 0000000..7ad8adf
--- /dev/null
+++ b/flask_marshmallow/schema.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+import flask
+import marshmallow as ma
+
+class Schema(ma.Schema):
+ """Base serializer with which to define custom serializers.
+
+ See `marshmallow.Schema` for more details about the `Schema` API.
+ """
+
+ def jsonify(self, obj, many=False, *args, **kwargs):
+ """Return a JSON response containing the serialized data.
+
+
+ :param obj: Object to serialize.
+ :param bool many: Set to `True` if `obj` should be serialized as a collection.
+ :param kwargs: Additional keyword arguments passed to `flask.jsonify`.
+
+ .. versionchanged:: 0.6.0
+ Takes the same arguments as `marshmallow.Schema.dump`. Additional
+ keyword arguments are passed to `flask.jsonify`.
+ """
+ data = self.dump(obj, many=many).data
+ return flask.jsonify(data, *args, **kwargs)
diff --git a/flask_marshmallow/sqla.py b/flask_marshmallow/sqla.py
new file mode 100644
index 0000000..152a2f5
--- /dev/null
+++ b/flask_marshmallow/sqla.py
@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*-
+"""Integration with Flask-SQLAlchemy and marshmallow-sqlalchemy."""
+
+import marshmallow_sqlalchemy as msqla
+from .schema import Schema
+
+class DummySession(object):
+ """Placeholder session object."""
+ pass
+
+class SchemaOpts(msqla.SchemaOpts):
+ """Schema options for `ModelSchema `.
+ Same as `marshmallow_sqlalchemy.SchemaOpts`, except that we add a
+ placeholder `DummySession` if ``sqla_session`` is not defined on
+ class Meta. The actual session from `flask_sqlalchemy` gets bound
+ in `init_app`.
+ """
+ session = DummySession()
+
+ def __init__(self, meta):
+ if not hasattr(meta, 'sqla_session'):
+ meta.sqla_session = self.session
+ super(SchemaOpts, self).__init__(meta)
+
+class ModelSchema(msqla.ModelSchema, Schema):
+ """ModelSchema that generates fields based on the
+ `model` class Meta option, which should be a
+ ``db.Model`` class from `flask_sqlalchemy`. Uses the
+ scoped session from Flask-SQLAlchemy by default.
+
+ See `marshmallow_sqlalchemy.ModelSchema` for more details
+ on the `ModelSchema` API.
+ """
+ OPTIONS_CLASS = SchemaOpts
diff --git a/tasks.py b/tasks.py
index 2c68a7e..68bc2f1 100644
--- a/tasks.py
+++ b/tasks.py
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
import os
import sys
+import webbrowser
from invoke import task, run
@@ -25,19 +26,39 @@ def clean_docs():
@task
def browse_docs():
- run("open %s" % os.path.join(build_dir, 'index.html'))
+ path = os.path.join(build_dir, 'index.html')
+ webbrowser.open_new_tab(path)
@task
-def docs(clean=False, browse=False):
+def docs(clean=False, browse=False, watch=False):
+ """Build the docs."""
if clean:
clean_docs()
run("sphinx-build %s %s" % (docs_dir, build_dir), pty=True)
if browse:
browse_docs()
+ if watch:
+ watch_docs()
+
+@task
+def watch_docs():
+ """Run build the docs when a file changes."""
+ try:
+ import sphinx_autobuild # noqa
+ except ImportError:
+ print('ERROR: watch task requires the sphinx_autobuild package.')
+ print('Install it with:')
+ print(' pip install sphinx-autobuild')
+ sys.exit(1)
+ docs()
+ run('sphinx-autobuild {} {}'.format(docs_dir, build_dir), pty=True)
+
@task
def readme(browse=False):
run('rst2html.py README.rst > README.html')
+ if browse:
+ webbrowser.open_new_tab('README.html')
@task
def publish(test=False):
diff --git a/test_flask_marshmallow.py b/test_flask_marshmallow.py
index 9e7ab71..0aa3934 100755
--- a/test_flask_marshmallow.py
+++ b/test_flask_marshmallow.py
@@ -7,6 +7,8 @@
from flask_marshmallow import Marshmallow
from flask_marshmallow.fields import _tpl
+from flask_sqlalchemy import SQLAlchemy
+
import marshmallow
IS_MARSHMALLOW_2 = int(marshmallow.__version__.split('.')[0]) >= 2
@@ -217,3 +219,68 @@ def test_links_within_nested_object(app, mockbook):
author = result.data['author']
assert author['links']['self'] == url_for('author', id=mockbook.author.id)
assert author['links']['collection'] == url_for('authors')
+
+class TestSQLAlchemy:
+
+ @pytest.yield_fixture()
+ def extapp(self):
+ app_ = Flask('extapp')
+ app_.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
+ SQLAlchemy(app_)
+ Marshmallow(app_)
+ ctx = _app.test_request_context()
+ ctx.push()
+
+ yield app_
+
+ ctx.pop()
+
+ @pytest.fixture()
+ def db(self, extapp):
+ return extapp.extensions['sqlalchemy'].db
+
+ @pytest.fixture()
+ def extma(self, extapp):
+ return extapp.extensions['flask-marshmallow']
+
+ @pytest.yield_fixture()
+ def models(self, db):
+ class AuthorModel(db.Model):
+ __tablename__ = 'author'
+ id = db.Column(db.Integer, primary_key=True)
+ name = db.Column(db.String(255))
+
+ class BookModel(db.Model):
+ __tablename__ = 'book'
+ id = db.Column(db.Integer, primary_key=True)
+ title = db.Column(db.String(255))
+ author_id = db.Column(db.Integer, db.ForeignKey('author.id'))
+ author = db.relationship('AuthorModel', backref='books')
+
+ db.create_all()
+
+ class _models:
+ def __init__(self):
+ self.Author = AuthorModel
+ self.Book = BookModel
+ yield _models()
+ db.drop_all()
+
+ def test_can_declare_model(self, extma, models, db):
+ class AuthorSchema(extma.ModelSchema):
+ class Meta:
+ model = models.Author
+
+ author_schema = AuthorSchema()
+
+ author = models.Author(name='Chuck Paluhniuk')
+
+ db.session.add(author)
+ db.session.commit()
+ result = author_schema.dump(author)
+ assert 'id' in result.data
+ assert 'name' in result.data
+ assert result.data['name'] == 'Chuck Paluhniuk'
+
+ resp = author_schema.jsonify(author)
+ assert isinstance(resp, BaseResponse)