Creating MapFish Views¶
Papyrus provides an implementation of the MapFish Protocol. This implementation relies on GeoAlchemy.
This section provides an example describing how to build a MapFish web service in a Pyramid application. (A MapFish web service is a web service that conforms to the MapFish Protocol.)
We assume here that we want to create a MapFish web service named spots
that relies on a database table of the same name.
Setting up the db model¶
First of all we need an SQLAlchemy/GeoAlchemy mapping for that table. To comply
with Papyrus’ MapFish Protocol implementation the mapped class must implement
the Python Geo Interface
(typically through the __geo_interface__ property), and must define
__init__ and __update__ methods.
Implementing the Python Geo Interface is required for Papyrus to be able to
serialize Spot objects into GeoJSON. The __init__ and __update__
methods are required for inserting and updating objects, respectively. Both the
__init__ and __update__ methods receive a GeoJSON feature
(geojson.Feature) as their first arguments.
GeoInterface¶
Papyrus provides a class to help create SQLAlchemy/GeoAlchemy mapped classes
that implement the Python Geo Interface, and define __init__ and
__update__ as expected by the MapFish protocol. This class is
papyrus.geo_interface.GeoInterface.
The GeoInterface class can be used as the super class of the user-defined
class. For example:
from sqlalchemy.ext.declarative import declarative_base
from papyrus.geo_interface import GeoInterface
Base = declarative_base()
class Spot(GeoInterface, Base):
__tablename__ = 'spots'
id = Column(Integer, primary_key=True)
name = Column(Unicode, nullable=False)
geom = GeometryColumn('the_geom', Point(srid=4326))
# For SQLAlchemy/GeoAlchemy to be able to create the geometry
# column when Spot.__table__.create or metadata.create_all is
# called.
GeometryDDL(Spot.__table__)
Or it can be used as the base class of classes generated by SQLAlchemy’s declarative layer. For example:
from sqlalchemy.ext.declarative import declarative_base
from papyrus.geo_interface import GeoInterface
# constructor=None is required for declarative_base to not
# provide its own __init__ constructor
Base = declarative_base(cls=GeoInterface, constructor=None)
class Spot(Base):
__tablename__ = 'spots'
id = Column(Integer, primary_key=True)
name = Column(Unicode, nullable=False)
geom = GeometryColumn('the_geom', Point(srid=4326))
# For SQLAlchemy/GeoAlchemy to be able to create the geometry
# column when Spot.__table__.create or metadata.create_all is
# called.
GeometryDDL(Spot.__table__)
GeoInterface represents a convenience method. Implementing one’s own
__geo_interface__, __init__, and __update__ definitions may
be a better choice than relying on GeoInterface.
Note
When using GeoInterface understanding its code
can be useful. It can also be a source of inspiration for those who don’t
use it.
One can change the behavior of GeoInterface by overloading its
__init__, __update__, and __read__ functions. The latter is called
by the __geo_interface__ property, and is therefore the one to overload to
change the behavior of __geo_interface__.
By default __read__ reads from column properties
only. Likewise, __update__ writes to column properties only. Other property
types are ignored. To make __read__ and __update__ consider other
properties the __add_properties__ class-level property can be used. This
property should reference a collection of property names. For example:
from papyrus.geo_interface import GeoInterface
class Type(Base):
__tablename__ = 'type'
id = Column(Integer, primary_key=True)
name = Column(Unicode, nullable=False)
class Spot(GeoInterface, Base):
__tablename__ = 'spots'
id = Column(Integer, primary_key=True)
name = Column(Unicode, nullable=False)
geom = GeometryColumn('the_geom', Point(srid=4326))
type_id = Column(Integer, ForeignKey('type.id'))
type_ = relationship(Type)
type = association_proxy('type_', 'name')
__add_properties__ = ('type',)
With the above code features returned by the __geo_interface__ will include
type properties. And __update__ will set type in Spot object
being updated.
Without GeoInterface¶
Without using GeoInterface our Spot class could look like this:
class Spot(Base):
__tablename__ = 'spots'
id = Column(Integer, primary_key=True)
name = Column(Unicode, nullable=False)
geom = GeometryColumn('the_geom', Point(srid=4326))
def __init__(self, feature):
self.id = feature.id
self.__update__(feature)
def __update__(self, feature):
geometry = feature.geometry
if geometry is not None and \
not isinstance(geometry, geojson.geometry.Default):
shape = asShape(geometry)
self.geom = WKBSpatialElement(buffer(shape.wkb), srid=4326)
self._shape = shape
self.name = feature.properties.get('name', None)
@property
def __geo_interface__(self):
id = self.id
if hasattr(self, '_shape') and self._shape is not None:
geometry = self_shape
else:
geometry = loads(str(self.geom.geom_wkb))
properties = dict(name=self.name)
return geojson.Feature(id=id, geometry=geometry, properties=properties)
# For SQLAlchemy/GeoAlchemy to be able to create the geometry
# column when Spot.__table__.create or metadata.create_all is
# called.
GeometryDDL(Spot.__table__)
Notes:
- the
pyramid_routesalchemytemplate, provided by Pyramid, places SQLAlchemy models in amodels.pyfile located at the root of the application’s main module (myapp.models). - the
akhettemplate, provided by the Akhet package, places SQLAlchemy models in the__init__.pyfile of themodelsmodule.
Setting up the web service¶
Now that database model is defined we can now create the core of our MapFish web service.
The web service can be defined through view callables, or through an
handler class. View callables are a concept of Pyramid itself. Handler
classes are a concept of the pyramid_handlers package, which is an official
Pyramid add-on.
With view callables¶
Using view functions here’s how our web service implementation would look like:
from myproject.models import Session, Spot
from papyrus.protocol import Protocol
# 'geom' is the name of the mapped class' geometry property
proto = Protocol(Session, Spot, 'geom')
@view_config(route_name='spots_read_many', renderer='geojson')
def read_many(request):
return proto.read(request)
@view_config(route_name='spots_read_one', renderer='geojson')
def read_one(request):
id = request.matchdict.get('id', None)
return proto.read(request, id=id)
@view_config(route_name='spots_count', renderer='string')
def count(request):
return proto.count(request)
@view_config(route_name='spots_create', renderer='geojson')
def create(request):
return proto.create(request)
@view_config(route_name='spots_update', renderer='geojson')
def update(request):
id = request.matchdict['id']
return proto.update(request, id)
@view_config(route_name='spots_delete')
def delete(request):
id = request.matchdict['id']
return proto.delete(request, id)
@view_config(route_name='spots_md', renderer='xsd')
def md(request):
return Spot.__table__
View functions are typically defined in a file named views.py. The first
six views define the MapFish web service. The seventh view (md) provides
a metadata view of the Spot model/table.
We now need to provide routes to these actions. This is done by calling
add_papyrus_routes() on the Configurator (in __init__.py):
import papyrus
from papyrus.renderers import GeoJSON, XSD
config.include(papyrus.includeme)
config.add_renderer('geojson', GeoJSON())
config.add_renderer('xsd', XSD())
config.add_papyrus_routes('spots', '/spots')
config.add_route('spots_md', '/spots/md.xsd', request_method='GET')
config.scan()
add_papyrus_routes is a convenience method, here’s what it basically
does:
config.add_route('spots_read_many', '/spots', request_method='GET')
config.add_route('spots_read_one', '/spots/{id}', request_method='GET')
config.add_route('spots_count', '/spots/count', request_method='GET')
config.add_route('spots_create', '/spots', request_method='POST')
config.add_route('spots_update', '/spots/{id}', request_method='PUT')
config.add_route('spots_delete', '/spots/{id}', request_method='DELETE')
With a handler¶
Using a handler here’s what our web service implementation would look like:
from pyramid_handlers import action
from myproject.models import Session, Spot
from papyrus.protocol import Protocol
# create the protocol object. 'geom' is the name
# of the geometry attribute in the Spot model class
proto = Protocol(Session, Spot, 'geom')
class SpotHandler(object):
def __init__(self, request):
self.request = request
@action(renderer='geojson')
def read_many(self):
return proto.read(self.request)
@action(renderer='geojson')
def read_one(self):
id = self.request.matchdict.get('id', None)
return proto.read(self.request, id=id)
@action(renderer='string')
def count(self):
return proto.count(self.request)
@action(renderer='geojson')
def create(self):
return proto.create(self.request)
@action(renderer='geojson')
def update(self):
id = self.request.matchdict['id']
return proto.update(self.request, id)
@action()
def delete(self):
id = self.request.matchdict['id']
return proto.delete(self.request, id)
@action(renderer='xsd')
def md(self):
return Spot.__table__
The six actions of the SpotHandler class entirely define our MapFish web
service.
We now need to provide routes to these actions. This is done by calling
add_papyrus_handler() on the Configurator:
import papyrus
from papyrus.renderers import GeoJSON
config.include(papyrus)
config.add_renderer('geojson', GeoJSON())
config.add_papyrus_handler('spots', '/spots',
'myproject.handlers:SpotHandler')
config.add_handler('spots_md', '/spots/md.xsd',
'myproject.handlers:SpotHandler', action='md',
request_method='GET')
Likewise add_papyrus_routes add_papyrus_handler is a convenience
method. Here’s what it basically does:
config.add_handler('spots_read_many', '/spots',
'myproject.handlers:SpotHandler',
action='read_many', request_method='GET')
config.add_handler('spots_read_one', '/spots/{id}',
'myproject.handlers:SpotHandler',
action='read_one', request_method='GET')
config.add_handler('spots_count', '/spots/count',
'myproject.handlers:SpotHandler',
action='count', request_method='GET')
config.add_handler('spots_create', '/spots',
'myproject.handlers:SpotHandler',
action='create', request_method='POST')
config.add_handler('spots_update', '/spots/{id}',
'myproject.handlers:SpotHandler',
action='update', request_method='PUT')
config.add_handler('spots_delete', '/spots/{id}',
'myproject.handlers:SpotHandler',
action='delete', request_method='DELETE')
Note: when using handlers the pyramid_handlers package must be set as an
application’s dependency.