Skip to main content

Geospatial Extensions for Pyramid

Project description

Geospatial Extensions for the Pyramid web framework.

Install

Papyrus can be installed with easy_install:

$ easy_install papyrus

(Installing Papyrus in an isolated virtualenv is recommended.)

Often you’ll want to make Papyrus a dependency of your Pyramid application. For that add papyrus to the install_requires list defined in the Pyramid application setup.py. Example:

install_requires = [
    'pyramid',
    'pyramid_handlers',
    'pyramid_tm',
    'SQLAlchemy',
    'WebError',
    'papyrus'
    ]

Notes:

  • the pyramid_handlers package is required for creating handlers and actions, in place view callable. Handlers basically emulate Pylons’ controllers, so people coming from Pylons may want to use pyramid_handlers in their Pyramid applications.

Run Papyrus Tests

To run the Papyrus tests additional packages need to be installed, namely nose, mock, psycopg2, simplejson, pyramid_handlers, and coverage. There’s no need to manually install these packages, as they will be installed when the setup.py nosetests command is run.

However, for these packages to install correctly, you have to have header files for PostgreSQL, Python, and GEOS. On Debian-based systems install the following system packages: libpq-dev, python-dev, libgeos-c1.

To run the tests:

$ python setup.py nosetests

Currently, 100% of the Papyrus code is covered by tests, I’d like to preserve that.

GeoJSON Renderer

Papyrus provides a GeoJSON renderer, based on Sean Gillies’ geojson package.

To use it the GeoJSON renderer factory must be added to the application configuration.

For that you can either pass the factory to the Configurator constructor:

from pyramid.mako_templating import renderer_factory as mako_renderer_factory
from papyrus.renderers import GeoJSON
config = Configurator(
    renderers=(('.mako', mako_renderer_factory),
               ('geojson', GeoJSON()))
    )

Or you can apply the add_renderer method to the Configurator instance:

from papyrus.renderers import GeoJSON
config.add_renderer('geojson', GeoJSON())

Make sure that add_renderer is called before any add_view call that names geojson as an argument.

To use the GeoJSON renderer in a view set renderer to geojson in the view config. Here is a simple example:

@view_config(renderer='geojson')
def hello_world(request):
    return {
        'type': 'Feature',
        'id': 1,
        'geometry': {'type': 'Point', 'coordinates': [53, -4]},
        'properties': {'title': 'Dict 1'},
        }

Views configured with the geojson renderer must return objects that implement the Python Geo Interface.

Here’s another example where the returned object is an SQLAlchemy (or GeoAlchemy) mapped object:

@view_config(renderer='geojson')
def features(request):
    return Session().query(Spot).all()

In the above example the Spot objects returned by the query call must implement the Python Geo Interface.

Notes:

  • The GeoJSON renderer requires simplejson 2.1 or higher. Indeed, to be able to deal with decimal.Decimal values, which are common when using SQLAlchemy, we set use_decimal to True when calling the dumps function, and only simplejson 2.1 and higher support that argument.

  • The GeoJSON renderer supports JSONP. The renderer indeed checks if there’s a callback parameter in the query string, and if there’s one it wraps the response in a JavaScript call and sets the response content type to text/javascript.

  • The application developer can also specify the name of the JSONP callback parameter, using this:

    from papyrus.renderers import GeoJSON
    config.add_renderer('geojson', GeoJSON(jsonp_param_name='cb'))

    With this, if there’s a parameter named cb in the query string, the renderer will return a JSONP response.

  • By default, lists and tuples passed to the renderer will be rendered as FeatureCollection. You can change this using the collection_type argument:

    from papyrus.renderers import GeoJSON
    config.add_renderer('geojson', GeoJSON(collection_type='GeometryCollection'))

XSD Renderer

Papyrus provides an XSD renderer, capable of serializing SQLAlchemy Table objects (including GeoAlchemy geometry columns) into XML Schema Documents.

XSDs generated by the XSD Renderer can, for example, be parsed using OpenLayers’s DescribeFeatureType format.

To use the XSD render the XSD renderer factory should be added to the application configuration.

This is done by either passing the factory to the Configurator constructor:

from pyramid.mako_templating import renderer_factory as mako_renderer_factory
from papyrus.renderers import XSD
config = Configurator(
    renderers=(('.mako', mako_renderer_factory),
               ('xsd', XSD()))
    )

Or by applying the add_renderer method to the Configurator instance:

from papyrus.renderers import XSD
config.add_renderer('geojson', XSD())

Make sure that add_renderer is called before any add_view call that names xsd as an argument.

To use the XSD renderer in a view set renderer to xsd in the view config. Here is a simple example:

from sqlalchemy import Table, MetaData, Column, types

@view_config(renderer='xsd')
def hello_world(request):
    return Table('table', MetaData(),
                 Column('id', types.Integer, primary_key=True)
                 )

Views configured with the xsd renderer should return SQLAlchemy Table objects.

Here’s another example:

@view_config(renderer='xsd')
def features(request):
    return Spot.__table__

Where Spot is an SQLAlchemy mapped class created using SQLAlchemy’s declarative layer.

MapFish Web Services

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 an web service that conforms to the MapFish Protocol.)

We assume we want to create a spots MapFish web service that relies on a spots database table.

Set up the database Model

First of all we need an SQLAlchemy/GeoAlchemy mapping for that table. To be compliant 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 being able to serialize Spot objects into GeoJSON (using Papyrus’ GeoJSON renderer). 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 an argument.

With GeoInterface

Papyrus provides a mixin to help create SQLAlchemy/GeoAlchemy mapped classes that implement the Python Geo Interface, and define __init__ and __update__ as expected by the MapFish protocol. The mixin is named GeoInterface, and is provided by the papyrus.geo_interface module.

Using GeoInterface our Spot class looks like this:

from papyrus.geo_interface import GeoInterface

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__)

GeoInterface represents a convenience method. Often, implementing one’s own __geo_interface__, __init__, and __update__ definitions is a better choice than relying on GeoInterface.

When using GeoInterface understanding its code can be useful. It can also be a source of inspiration for those who don’t use it.

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_routesalchemy template, provided by Pyramid, places SQLAlchemy models in a models.py file located at the root of the application’s main module (myapp.models).

  • the akhet template, provided by the Akhet package, places SQLAlchemy models in the __init__.py file of the models module.

Set 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.

TODO

Changes

0.8

  • Add a XSD renderer for WFS DescribeFeatureType-like responses. Thanks @twpayne.

0.7

  • Make feature argument optional in the GeoInterface constructor. This change allow better integration with FormAlchemy for classes that implement the GeoInterface.

0.6

  • When passed a list or a tuple the GeoJSON renderer produces a FeatureCollection by default. This behavior can be changed with the collection_type argument to GeoJSON (patch from @tonio).

  • Pyramid 1.2 compliance (a change in the tests)

0.5

  • JSONP support in the GeoJSON renderer (patch from @sbrunner)

  • New GeoJSON renderer implementation and API. The papyrus.renderers.geojson_renderer_factory function is replaced by the papyrus.renderers.GeoJSON class. The new usage is:

    from papyrus.renderers import GeoJSON
    config.add_renderer('geojson', GeoJSON(jsonp_param_name='cb'))
  • Pyramid 1.1 compliance

0.4

  • Improved GeoJSON renderer: deal with decimal.Decimal, datetime.date, and datetime.datetime values.

  • No longer use <= when defining requirements (only >= is used).

  • Correctly spell the names of requirements, using capital letters where needed.

0.3.1

  • Add MANIFEST.in file

0.3

  • Papyrus can now be used without pyramid_handlers

  • add a config method to add routes, pyramid.add_papyrus_routes

  • do not rely on environ['CONTENT_LENGTH'] to read the contents of requests, this doesn’t work with WebOb 1.0.4 and higher

  • minor change in the tests to use with_statement for Python 2.5

0.2

  • Add the papyrus.geo_inteface.GeoInterface mixin

  • Add the papyrus.add_papyrus_handler configurator directive

0.1

  • Initial version

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

papyrus-0.8.tar.gz (25.8 kB view details)

Uploaded Source

File details

Details for the file papyrus-0.8.tar.gz.

File metadata

  • Download URL: papyrus-0.8.tar.gz
  • Upload date:
  • Size: 25.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No

File hashes

Hashes for papyrus-0.8.tar.gz
Algorithm Hash digest
SHA256 d97bb451106deff6f16a42b0a4fd9b4fbc1d6a914088f346b71a9b41d6f6730b
MD5 37275383fe0918e10a7e7ed4bfbe65a5
BLAKE2b-256 ff62679d1cd064e3b155099506f807b03dcc77017cf025c68fe7b04e2fbfd96f

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page