With Pyramid¶
Since version 1.7 there are some Pyramid specific subclasses that help using the proxies within a Pyramid view as well as a expose decorator that simplify their implementation.
- class metapensiero.sqlalchemy.proxy.pyramid.expose(proxable, metadata=None, adaptor=None, POST=True, **kw)¶
Decorator to simplify exposition of a SQLAlchemy Query.
- Parameters:
proxable – either a SQLAlchemy Query or a mapped class
metadata – a dictionary with additional info about the fields
adaptor – if given, it’s a function that will be called to adapt incoming data before actually writing it to the database.
POST (either a boolean flag or a function,
True
by default) – whether to handle POST request: ifTrue
a standard function will be used, otherwise it must be a function accepting two positional arguments, respectively the SQLAlchemy session and the Pyramid request object, and a set of keyword arguments corresponding to the changed field
This is an helper class that aids the exposition of either a SQLAlchemy Query or directly a mapped class as a Pyramid view.
User of this class must inject a concrete implementation of the
create_session()
andsave_changes()
static methods. This is usually done once at application startup, for example:from ..models import DBSession from ..models.utils import save_changes # Configure the `expose` decorator expose.create_session = staticmethod(lambda req: DBSession()) expose.save_changes = staticmethod(save_changes)
Another class method that may eventually be replaced is
extract_parameters()
: the default implementation simply returns a copy of the request.params dictionary, but sometimes it is desiderable to pass additional parameters, for example when using bindparams:def _extract_parameters(request): "Build a dictionary of arguments for the proxy from the current request" parameters = dict(request.params) # The following feeds eventual `bindparams` parameters['params'] = dict(request.session) return parameters expose.extract_parameters = staticmethod(_extract_parameters)
The typical usage is:
@view_config(route_name='users', renderer='json') @expose(User, metadata=dict( password=dict(hidden=True, password=True, width=40), is_anonymous=False, )) def users(request, results): return results
The first argument may be either a mapped class or a query.
The decorated function is finally called with the current request and the result of the operation, and it can eventually adjust the results dictionary.
The decorated function may be a generator instead, which has the opportunity of freely manipulate either the arguments received from the request, or the final result, or both as follows:
@expose(User, metadata=dict( password=dict(hidden=True, password=True, width=40), is_anonymous=False, )) def complex(): # Receive request and params request, params = (yield) log.debug('REQUEST: %r', request) # Adjust parameters params['new'] = True if 'something' in params: # Inject other conditions something = params.pop('something') conditions = (User.c.foo == something,) result = yield params, conditions else: # Go on, and receive the final result result = yield params # Fix it up result['COMPLEX'] = 'MAYBE' yield result
As you can see, in this case the decorated function shall not declare any formal argument, because it receives its “feed” as the result of the
yield
expressions.- static create_session(request)¶
Create a new SQLAlchemy session, given the current request.
- static extract_parameters(request)¶
Create a dictionary of parameters from the current request.
- static save_changes(sa_session, modified, deleted)¶
Save insertions, changes and deletions to the database.
- Parameters:
sa_session – the SQLAlchemy session
modified – a sequence of record changes, each represented by a tuple of two items, the PK name and a dictionary with the modified fields; if the value of the PK field is null or 0 then the record is considered new and will be inserted instead of updated
deleted – a sequence of deletions, each represented by a tuple of two items, the PK name and the ID of the record to be removed
- Return type:
a tuple of three lists, respectively inserted, modified and deleted record IDs, grouped in a dictionary keyed on PK name.
Basic setup¶
First of all, there are some setup steps to follow:
Include the package in the configuration file:
[app:main] use = egg:ph.data ... pyramid.includes = metapensiero.sqlalchemy.proxy.pyramid pyramid_tm
This is not strictly needed, but it will override the standard
json
renderer with one that uses python-rapidjson, to handle the datetime type.Configure the
expose
decorator, for example adding something like the following snippet to the.../views.py
source:from metapensiero.sqlalchemy.proxy.pyramid import expose from .models import DBSession # Configure the `expose` decorator expose.create_session = staticmethod(lambda req: DBSession())
Then you can add views to expose either an entity or a plain select:
@view_config(route_name='users', renderer='json')
@expose(User, metadata=dict(
password=dict(hidden=True, password=True, width=40),
active=dict(default=True),
is_anonymous=False,
))
def users(request, results):
return results
sessions_t = Session.__table__
@view_config(route_name='sessions', renderer='json')
@expose(select([sessions_t], sessions_t.c.iduser == bindparam('user_id')))
def sessions(request, results):
return results
The decorated function may be a generator instead, which has the opportunity of freely manipulate either the arguments received from the request, or the final result, or both as follows:
@view_config(route_name='users', renderer='json')
@expose(User, metadata=dict(
password=dict(hidden=True, password=True, width=40),
active=dict(default=True),
is_anonymous=False,
))
def complex():
# Receive request and arguments
request, args = (yield)
# Adjust parameters
args['new'] = True
# Note that bindparams by default are extracted from the “params”
# keyword argument
bindparams = args.setdefault('params', {})
bindparams['user_id'] = 2
if 'something' in params:
# Inject other conditions
something = args.pop('something')
conditions = (User.c.foo == something,)
result = yield args, conditions
else:
# Go on, and receive the final result
result = yield args
# Fix it up
result['COMPLEX'] = 'MAYBE'
yield result
Request examples¶
Assuming the users
view is added as /views/users
, it could be called in the following
ways:
GET /views/users
would return a JSON response containing all users, like:
{ "count": 1234, "message": "Ok", "success": true, "root": [ { "first_name": "Lele", "last_name": "Gaifax", ... }, { "first_name": "Mario", "last_name": "Rossi", ... }, ... ] }
GET /views/users?limit=1&start=2
would return a JSON response containing just one user, the second:
{ "count": 1234, "message": "Ok", "success": true, "root": [ { "first_name": "Mario", "last_name": "Rossi", ... } ] }
GET /views/users?filter_by_first_name=Lele
would return a JSON response containing the records satisfying the given condition:
{ "count": 1, "message": "Ok", "success": true, "root": [ { "first_name": "Lele", "last_name": "Gaifax", ... } ] }
GET /views/users?filter_col=first_name&filter_value=Lele
same as above
GET /views/users?limit=1&only_cols=first_name,role_name
would return a JSON response containing only the requested fields of a single record:
{ "count": 1234, "message": "Ok", "success": true, "root": [ { "first_name": "Lele", "role_name": "administrator" } ] }
GET /views/users?metadata=metadata&limit=0
would return a JSON response containing a description of the schema:
{ "metadata": { "success_slot": "success", "primary_key": "iduser", "fields": [ { "width": 60, "hint": "The unique ID of the user.", "align": "right", "nullable": false, "readonly": true, "type": "int", "hidden": true, "label": "User ID", "name": "iduser" }, { "type": "boolean", "label": "Active", "hint": "Whether the user is currently active.", "name": "active", "default": true ... } ... ], "root_slot": "root", "count_slot": "count" }, "message": "Ok", "success": true }
Browse SoL sources for real usage examples.
Obtaining original instances¶
By default the decorator configures the proxy to return plain Python dictionaries, but sometime you may need to manipulate the resultset in way that it is easier done working with the actual instances.
In such cases you can explicitly pass asdict=False
and then convert the list of instances
to a JSON-serializable list of dictionaries by yourself, for example with something like the
following, where the Image
class is using sqlalchemy-media to store image details in
the image
field, backed by a JSONB
column, of the class:
@view_config(route_name='images', renderer='json')
@expose(
Image,
asdict=False, fields=('id', 'position', 'width', 'height', 'original_filename'),
metadata=dict(
height=dict(label=_('Height'),
hint=_('The height of the image.')),
original_filename=dict(label=_('Name'),
hint=_('Original name of the image.')),
width=dict(label=_('Width'),
hint=_('The width of the image.'))))
def listing(request, results):
root = results.get('root')
if root:
results['root'] = newroot = []
for image in root:
img = image.image
d = {'id': image.id, 'position': image.position,
'width': img['width'], 'height': img['height'],
'original_filename': img['original_filename']}
thumbnail = img.get_thumbnail(width=200, auto_generate=True)
d['image_url'] = thumbnail.locate()
newroot.append(d)
return results