A Flask ModelView that makes it a bit easier to manage views for SQLAlchemy Declarative models. The AlchemyView class extends the very nice Flask-Classy FlaskView and supports all Flask-Classy FlaskView functionality.
Flask-AlchemyView uses colander for validation and dictalchemy for updating/creating/reading models.
Note
New in 0.1.4
Flask-AlchemyView passes all tests with Python 3.3.2. However, at the time of writing (2013-09-06) there is a bug in Flask-SQLAlchemy that hasn’t been fixed in a release so in order to use Flask-AlchemyView with Flask-SQLAlchemy under Python 3.3.2 one must install an unreleased version of Flask-AlchemyView.
Installing unreleased Flask-SQLAlchemy version:
pip install -e git+https://github.com/mitsuhiko/flask-sqlalchemy#egg=Flask-SQLAlchemy
To summarize, Python3 is not well tested and only against 3.3.2 but it should work if Flask-SQLAlchemy works.
A AlchemyView uses either AlchemyView.session or, if that is not set, the session from Flask-SQLAlchemy(new since v0.1.3). The prefered way to access the session is to use the Flask-SQLAlchemy session.
This is not recommended but if Flask-SQLAlchemy is not used the session can be set on AlchemyView directly. Be aware of that this will probably create problems if AlchemyView is used by several applications.
Nothing needs to be done in order to use Flask-SQLAlchemy. However, db.Model should be made dictable.
Setup Flask-SQLAlchemy:
...
from flask import Flask
app = Flask(__name__)
from flask.ext.sqlalchemy import SQLAlchemy
db = SQLAlchemy(app)
from dictalchemy import make_class_dictable
make_class_dictable(db.Model)
Note
New in 0.1.4
AlchemyView will return json or HTML depending on the HTTP Accept Header. When returning HTML the template used will be determined by AlchemyView._get_template_name(). The response data will be passed to the template in the parameter data.
If the client sends both application/json and text/html (or */*) and application/json and text/html has the same weight text/html will be assumed. This is because some browsers sends */*. This means that the client must send application/json with a greater weight than */* or text/html to recieve JSON. AngularJS for example does not do this, so in order to use angularjs and recieve JSON one must explicitly declare the accept header.
AngularJS example:
$http({method: 'GET', url: '/mymodel/', headers: {Accept: 'application/json'});
If the response is a 200 it’s possible to provide extra parameters to the template By adding a method called ‘before_<view method>_render’. If that method returns anything it must be a dict containing parameters that will be passed to the template.
Adding is_owner parameter to a GET request template:
def before_get_render(self, data):
return {'is_owner': data[owner_id'] === current_user.id}
If a template is missing a 406 will be returned, not a 500.
Note
New in 0.1.4
If an error occurs and json is returned a dict containing ‘message’ and ‘errors’ will be returned. If html is returned a BadRequest is raised.
Lets start with a simple model, a user:
class User(Base):
id = Column(Integer, primary_key)
name = Column(Unicode)
email = Column(Unicode)
The colander schema looks like this:
import colander as c
class UserSchema(c.MappingSchema):
name = c.SchemaNode(c.String())
email = c.SchemaNode(c.String())
The View:
class UserView(AlchemyView):
model = UserModel
schema = UserSchema
UserView.register(app)
After this the following routes has been defined:
- GET /user/[ID]
- POST /user/
- PUT /user/[ID]
- DELETE /user/[ID]
- GET /user/
So far so good, but that can easily be done without AlchemyView. So why use AlchemyView? Well, it’s pretty configurable. There is support for different schemas depending on weather a PUT or POST is made, it can follow relationships on GET, and to some extent on PUT and POST also. It can take limit, offset, sortby and direction arguments when listing.
In case of a GET item request the view will check if the actual item exists. If it does the AlchemyView will return that object in JSON form.
What is returned by GET(individual item or a list) is defined by the parameters AlchemyView.dict_params and AlchemyView.asdict_params. If non of them is set dictalchemy.utils.asdict() will be called on the model without parameters.
The method AlchemyView._base_query() can be overridden in order to add joins, exclude/include columns etc. The returned query is the one that will be used when performing a GET.
Returning only a specific attribute:
asdict_params = {'only': ['name']}
Following a relationship:
asdict_params = {'follow': {'group':{}}}
Adding a join to the query:
def _base_query(self):
return self.session.query(User).join(Group)
Updating an item is pretty basic. If the item exists it will be updated with the data returned by the update schema. The update schema is either AlchemyView.update_schema or AlchemyView.schema if update_schema isn’t set. If any SchemaNode in the schema returns colander.null it will be removed from the update data, None will be preserved. This behaviour cannot be modified at the moment.
Updating the item will be done by calling fromdict() on AlchemyView.model. The parameters will be AlchemyView.fromdict_params, or AlchemyView.dict_params if fromdict_params isn’t set.
On validation error a 400 will be returned, on other errors a 500 will be returned.
Out of the box a AlchemyView is a bit limited in it’s update/create functionality. This is by design, if creating/updating a model is more complex it’s best to not try to do it automagically.
When post:ing data the data will be validated by AlchemyView.create_schema or AlchemyView.schema if create_schema isn’t set. Colander null values will not be removed. The validated data will be sent to the model constructor. On validation error an error message will be returned, on other errors a 500 will be returned.
A delete will simply delete the instance if it exists. The delete method is defined as AlchemyView.delete() and AlchemyView._delete().
The listing URL takes the additional parameters limit, offset, sortby and direction. The View has a max_page_limit attribute that ensures that limit can’t be set to high.
If sortby isn’t set the sortby attribute will be used. It that is set to None no sorting will be done. The sortby argument is checked against sortby_map which is a map of string: expression. The expression must be something that can be inserted into the _base_query, so either a column or a valid string. If the sortby parameter is not found in sortby_map a 400 will be returned.
sortby_map Example:
sortby_map = {'name': User.name, 'group_id': 'Group.id'}
Bases: flask_classy.FlaskView
View for SQLAlchemy dictable models
The pre-defined methods will always return JSON, with the mimetype set to application/json.
The JSON Encoder that should be used to load/dump json
alias of _JSONEncoder
Get the base query that should be used
For example add joins here. Default implementation returns self._get_session().query(self.model).
Returns: | Sqlalchemy query |
---|
Get colander schema for create
Returns: | Colander schema for create data |
---|
Get item based on id
Can handle models with int and string primary keys.
Raises : | Exception if the primary key is a composite or not int or string |
---|---|
Returns: | An item, calls flask.abort(400) if the item isn’t found |
Get response type from response
Returns: | ‘application/json’ or ‘text/html’ |
---|
Get basic colander schema
This schema is used if create_schema or update_schema isn’t set.
Returns: | Colander schema for create or update schema |
---|
Get SQLAlchemy session
Raises : | An exception if self.session isn’t set and Flask-SQLAlchemy isn’t used |
---|---|
Returns: | SQLAlchemy session |
Get template name for a specific mimetype
The template name is by default get_route_base()/name.suffix, the suffixes are stored in AlchemyView.template_suffixes.
Returns: | string |
---|
Get colander update schema
Returns: | Colander schema for create data |
---|
Get the url to read an item
Raises : | Exception if the items primary key is a composite key |
---|
Load object from json string
Uses AlchemyView.JSONEncoder to dump the data.
Parameters: | obj – Object that should be dumped |
---|---|
Returns: | JSON string |
Get a json response
Parameters: | obj – Exception OR something that can used by AlchemyView._json_dumps() If this is an exception the status will be set to 400 if status is less than 400. |
---|
Get a response
If the response will be rendered with a template and the method ‘_TEMPLATE_template_vars’ is set that method will be called and the returnvalue will be added to the template parameters.
Raises : | If status is beteen 400 and 500 OR data is an exception a BadRequest will be raised. |
---|---|
Returns: | A json or html response, based on the request accept headers |
Parameters that will be used when getting an item
The parameters will be used in dictalchemy.utils.asdict().
Create schema
Delete an item
This is just an alias for AlchemyView._delete().
Will be used instead of AlchemyView.asdict_params and AlchemyView.from_params if they’re not set
Parameters that will be used when updating an item
The parameters will be used in dictalchemy.utils.fromdict().
Returns a list
The response look like this:
items: [...]
count: Integer
limit: Integer
offset: Integer
Max page limit
SQLAlchemy declarative model
Default page limit
Handles POST
This method will create a model with request data if the data was valid. It validates the data with AlchemyView._get_create_schema().
If everything was successful it will return a 303 redirect to the newly created item.
If any error except validation errors are encountered a 500 will be returned.
Returns: | A response |
---|---|
Return type: | flask.Response |
Handles PUT
If any error except validation errors are encountered a 500 will be returned.
Colander schema that will be used as both update_schema and create_schema if they aren’t set
The SQLAlchemy session
If not set the session will be taken from the Flask-SQLAlchemy extension. If that is missing the view will not work.
Default sort direction
Default sortby column
If not set no sortby will be applied by default in AlchemyView.index().
In order for sortby to have any effect it also needs to be set in AlchemyView.sortby_map
Map of string=>column for sortby
The values can be anything that will work together with the query returned by AlchemyView._base_query(). So if there is a join in the base query that column, or name of that colum can be mapped to a key in the sortby_map.
Suffixes for response types, currently ‘text/html’ is the only one supported
Update schema
Bases: werkzeug.exceptions.HTTPException
HTTPException class that also contains error data
This will be raised when a request is invalid, use Flask.errorhandler() to handle these for HTML responses.
Variables: | data – Dict explaining the error, contains the keys ‘message’ and ‘errors’ |
---|
The source is hosted on http://github.com/danielholmstrom/flask-alchemyview.
Flask-AlchemyView is released under the MIT license.