In this section, you will find a step-by-step guide on how to build a RESTful application with everest.
Suppose you want to write a program that helps a garden designer with composing lists of beautiful perennials and shrubs that she intends to plant in her customer’s gardens. Let’s call this fancy application “Plant Scribe”. In its simplest possible form, this application will have to handle customers, projects (per customer), sites (per project), and plant species (per site).
everest applications keep their value state in entity objects.
The first step on our way to the Plant Scribe application is therefore to decide which data we want to store in our entity model. We start with the customer:
1 2 3 4 5 6 7 8 9 10 11 12 | from everest.entities.base import Entity
from everest.entities.utils import slug_from_string
class Customer(Entity):
def __init__(self, first_name, last_name, **kw):
Entity.__init__(self, **kw)
self.first_name = first_name
self.last_name = last_name
@property
def slug(self):
return slug_from_string("%s-%s" % (self.last_name, self.first_name))
|
In our example, the Customer class inherits from the Entity class provided by everest. This is convenient, but not necessary; any class can participate in the entity model as long as it implements the everest.entities.interfaces.IEntity interface. Note, however, that this interface requires the presence of a slug attribute, which in the case of the customer entity is composed of the concatenation of the customer’s last and first name.
For each customer, we need to be able to handle an arbitrary number of projects:
1 2 3 4 5 6 7 8 9 10 11 12 | from everest.entities.base import Entity
from everest.entities.utils import slug_from_string
class Project(Entity):
def __init__(self, name, customer, **kw):
Entity.__init__(self, **kw)
self.name = name
self.customer = customer
@property
def slug(self):
return slug_from_string(self.name)
|
Note that the name attribute, which serves as the project entity slug, does not need to be unique among all projects, but just among all projects for a given customer.
Another noteworthy observation is that although the project references the customer, we do not (yet) have a way to access the projects associated with a given customer as an attribute of its customer entity. Avoiding such circular references allows us to keep our entity model simple, but we may be missing the convenience they offer. We will return to this issue a little later.
Each project is referenced by one or more planting sites:
1 2 3 4 5 6 7 8 9 10 11 12 | from everest.entities.base import Entity
from everest.entities.utils import slug_from_string
class Site(Entity):
def __init__(self, name, project, **kw):
Entity.__init__(self, **kw)
self.name = name
self.project = project
@property
def slug(self):
return slug_from_string(self.name)
|
The plant species to choose from for each site are modeled as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | from everest.entities.base import Entity
from everest.entities.utils import slug_from_string
class Species(Entity):
def __init__(self, species_name, genus_name,
cultivar=None, author=None, **kw):
Entity.__init__(self, **kw)
self.species_name = species_name
self.genus_name = genus_name
self.cultivar = cultivar
self.author = author
@property
def slug(self):
return slug_from_string(
"%s-%s-%s-%s"
% (self.genus_name, self.species_name,
'' if self.cultivar is None else self.cultivar,
'' if self.author is None else self.author))
|
Finally, the information about which plant species to use at which site and in which quantity is modeled as an “incidence” entity:
1 2 3 4 5 6 7 8 9 10 11 12 | from everest.entities.base import Entity
class Incidence(Entity):
def __init__(self, species, site, quantity, **kw):
Entity.__init__(self, **kw)
self.species = species
self.site = site
self.quantity = quantity
@property
def slug(self):
return None if self.species is None else self.species.slug
|
With the entity model in place, we can now proceed to designing the resource layer. The first step here is to define the marker interfaces that everest will use to access the various parts of the resource system. This is very straightforward to do:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | """
This file is part of the everest project.
See LICENSE.txt for licensing, CONTRIBUTORS.txt for contributor information.
Created on Jan 9, 2012.
"""
from zope.interface import Interface # pylint: disable=F0401
__docformat__ = 'reStructuredText en'
__all__ = ['ICustomer',
'IIncidence',
'IProject',
'ISite',
'ISpecies',
]
# no __init__ pylint: disable=W0232
class ICustomer(Interface):
pass
class IProject(Interface):
pass
class ISpecies(Interface):
pass
class ISite(Interface):
pass
class IIncidence(Interface):
pass
# pylint: enable=W0232
|
Next, we move on to declaring the resource attributes using everest‘s resource attribute descriptors. Each resource attribute descriptor maps a single attribute from the resource’s entity and makes it available for access from the outside.
In our example application, the resources mostly declare the public attributes of the underlying entities as attributes:
1 2 3 4 5 6 7 8 9 10 | from everest.resources.base import Member
from everest.resources.descriptors import collection_attribute
from everest.resources.descriptors import terminal_attribute
from plantscribe.interfaces import IProject
class CustomerMember(Member):
relation = 'http://plantscribe.org/relations/customer'
first_name = terminal_attribute(str, 'first_name')
last_name = terminal_attribute(str, 'last_name')
projects = collection_attribute(IProject, backref='customer')
|
1 2 3 4 5 6 7 8 9 10 11 12 | from everest.resources.base import Member
from everest.resources.descriptors import collection_attribute
from everest.resources.descriptors import member_attribute
from everest.resources.descriptors import terminal_attribute
from plantscribe.interfaces import ICustomer
from plantscribe.interfaces import ISite
class ProjectMember(Member):
relation = 'http://plantscribe.org/relations/project'
name = terminal_attribute(str, 'name')
customer = member_attribute(ICustomer, 'customer')
sites = collection_attribute(ISite, backref='project', is_nested=True)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 | from everest.resources.base import Member
from everest.resources.descriptors import collection_attribute
from everest.resources.descriptors import member_attribute
from everest.resources.descriptors import terminal_attribute
from plantscribe.interfaces import IIncidence
from plantscribe.interfaces import IProject
class SiteMember(Member):
relation = 'http://plantscribe.org/relations/site'
name = terminal_attribute(str, 'name')
incidences = collection_attribute(IIncidence, backref='site',
is_nested=True)
project = member_attribute(IProject, 'project')
|
1 2 3 4 5 6 7 8 9 | from everest.resources.base import Member
from everest.resources.descriptors import terminal_attribute
class SpeciesMember(Member):
relation = 'http://plantscribe.org/relations/species'
species_name = terminal_attribute(str, 'species_name')
genus_name = terminal_attribute(str, 'genus_name')
cultivar = terminal_attribute(str, 'cultivar')
author = terminal_attribute(str, 'author')
|
1 2 3 4 5 6 7 8 9 10 11 | from everest.resources.base import Member
from everest.resources.descriptors import member_attribute
from everest.resources.descriptors import terminal_attribute
from plantscribe.interfaces import ISite
from plantscribe.interfaces import ISpecies
class IncidenceMember(Member):
relation = 'http://plantscribe.org/relations/incidence'
species = member_attribute(ISpecies, 'species')
site = member_attribute(ISite, 'site')
quantity = terminal_attribute(float, 'quantity')
|
In the simple case where the resource attribute descriptor declares a public attribute of the underlying entity, it expects a type or an interface of the target object and the name of the corresponding entity attribute as arguments.
For member_attribute() and collection_attribute() descriptors there is also an optional argument is_nested which determines if the URL for the target resource is going to be formed relative to the root (i.e., as an absolute path) or relative to the parent resource declaring the attribute.
We also have the possibility to declare resource attributes that do not reference the target resource directly through an entity attribute, but indirectly through a “backreferencing” attribute. In the example code, this is demonstrated in the projects attribute of the CustomerMember resource which allows us to access a customer’s projects at the resource level even though the underlying entity does not reference its projects directly.
With the resource layer in place, we can now move on to configuring our application. everest applications are based on the pyramid framework and everything you learned about configuring pyramid applications can be applied here. Rather than duplicating the excellent documentation available on the Pyramid web site, we will focus on a minimal example on how to configure the extra resource functionality that everest supplies.
The minimal .ini file for the plantscribe application is quite simple:
[DEFAULT]
[app:main]
paste.app_factory = plantscribe.run:app_factory
[server:main]
use = egg:Paste#http
host = 0.0.0.0
port = 6543
The only purpose of the .ini file is to specify a Paster application factory which is responsible for creating and setting up the application registry and for instantiating a WSGI application.
The .zcml configuration file - which is loaded through the application factory - is more interesting:
<configure xmlns="http://pylonshq.com/pyramid">
<!-- Include special directives. -->
<include package="everest.includes" />
<!-- Repositories. -->
<!-- Resource declarations. -->
<include file="resources.zcml" />
</configure>
Note the include directive at the top of the file; this not only pulls in the everest-specific ZCML directives, but also the Pyramid directives as well.
The most important of the everest-specific directives is the resource directive. This sets up the connections between the various parts of the resource subsystem, using our marker interfaces as the glue. At the minimum, you need to specify
The aggregate and collection objects needed by the resource subsystem (cf. xxx) are created automatically; you may, however, supply a custom collection class that inherits from everest.resources.base.Collection. If you do not plan on exposing the collection for this resource to the outside, you can set the expose flag to false, in which case you do not need to provide a root collection name. Non-exposed resources will still be available as a root collection internally, but access through the service as well as the generation of absolute URLs will not work.
To see our little application in action, we can use the pshell interactive shell that comes with Pyramid. First, install the plantscribe package by issuing
$ pip install -e .
inside the docs/demoapp/v0 folder of the everest source tree. This presumes you have followed the instructions of installing everest and use a virtualenv with the pip installer (cf. xxx).
Now, still from the same directory, you start the Pyramid pshell like this:
$ pshell plantscribe.ini
Python 2.7.2 (v2.7.2:8527427914a2, Jun 11 2011, 15:22:34)
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
Type "help" for more information.
Environment:
app The WSGI application.
registry Active Pyramid registry.
request Active request object.
root Root of the default resource tree.
root_factory Default root factory used to create `root`.
>>>
The root object that is available in the pshell environment is the service object that provides access to all public root collections by name:
>>> c = root['customers']
>>> c
<CustomerMemberCollection name:customers parent:Service(started)>
We can now start adding members to the collection and retrieve them back from the collection:
>>> from plantscribe.entities.customer import Customer
>>> ent = Customer('Peter', 'Fox')
>>> m = c.create_member(ent)
>>> m.__name__
'fox-peter'
>>> c.get('fox-peter').__name__
'fox-peter'
With the application running, we now turn our attention to persistency. everest uses a repository to load and save resources from and to a storage backend. To use a filesystem-based repository as the default for our application, we could use the following ZCML declaration:
<filesystem_repository
directory="data"
content_type="everest.mime.CsvMime"
make_default="true" />
This tells everest to use the data directory (relative to the plantscribe package) to persist representations of the root collections of all resources as .csv (Comma Separated Value) files. When the application is initialized, the root collections are loaded from these representation files and during each commit operation at the end of a transaction, all modified root collections are written back to their corresponding representation files.
The filesystem-based repository does not perform well with complex or high volume data structures or in cases where several processes need to access the same persistency backend. In these situations, we need to switch to a an ORM-based repository. everest uses xxx SQLAlchemy as ORM. What follows is a highly simplified account of what is needed to instruct SQLAlchemy to persist the entities of an everest application; for an explanation of the terms and concepts used in this section, please refer to the excellent documentation on the SQLAlchemy http://sqlalchemy.org web site.
In a first step, we need to initialize the ORM. The following ZCML declaration makes the ORM the default resource repository:
<orm_repository
metadata_factory="everest.tests.testapp_db.db.create_metadata"
make_default="true"/>
The metadata factory setting references a callable that takes an SQLAlchemy engine as a parameter and returns a fully initialized metadata instance. For our simple application, this function looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | """
This file is part of the everest project.
See LICENSE.txt for licensing, CONTRIBUTORS.txt for contributor information.
Created on Mar 27, 2012.
"""
from everest.orm import as_slug_expression
from everest.orm import mapper
from plantscribe.entities.customer import Customer
from plantscribe.entities.incidence import Incidence
from plantscribe.entities.project import Project
from plantscribe.entities.site import Site
from plantscribe.entities.species import Species
from sqlalchemy import Column
from sqlalchemy import Float
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy import MetaData
from sqlalchemy import String
from sqlalchemy import Table
from sqlalchemy.orm import relationship
from sqlalchemy.sql import literal
from sqlalchemy.sql import select
__docformat__ = 'reStructuredText en'
__all__ = []
def customer_slug(cls):
return as_slug_expression(cls.last_name + literal('-') + cls.first_name)
def project_slug(cls):
return as_slug_expression(cls.name)
def species_slug(cls):
return as_slug_expression(cls.genus_name + literal('-') +
cls.species_name + literal('-') +
cls.cultivar + literal('-') +
cls.author)
def site_slug(cls):
return as_slug_expression(cls.name)
def incidence_slug(cls):
return \
select([Species.slug]).where(cls.species_id == Species.id).as_scalar()
def create_metadata(engine):
# Create metadata.
metadata = MetaData()
# Define a database schema..
customer_tbl = \
Table('customer', metadata,
Column('customer_id', Integer, primary_key=True),
Column('first_name', String, nullable=False),
Column('last_name', String, nullable=False),
)
project_tbl = \
Table('project', metadata,
Column('project_id', Integer, primary_key=True),
Column('name', String, nullable=False),
Column('customer_id', Integer,
ForeignKey(customer_tbl.c.customer_id),
nullable=False),
)
site_tbl = \
Table('site', metadata,
Column('site_id', Integer, primary_key=True),
Column('name', String, nullable=False),
Column('project_id', Integer,
ForeignKey(project_tbl.c.project_id),
nullable=False),
)
species_tbl = \
Table('species', metadata,
Column('species_id', Integer, primary_key=True),
Column('species_name', String, nullable=False),
Column('genus_name', String, nullable=False),
Column('cultivar', String, nullable=False, default=''),
Column('author', String, nullable=False),
)
incidence_tbl = \
Table('incidence', metadata,
Column('site_id', Integer,
ForeignKey(site_tbl.c.site_id),
primary_key=True, index=True, nullable=False),
Column('species_id', Integer,
ForeignKey(species_tbl.c.species_id),
primary_key=True, index=True, nullable=False),
Column('quantity', Float, nullable=False),
)
# Map tables to entity classes.
mapper(Customer, customer_tbl,
id_attribute='customer_id', slug_expression=customer_slug)
mapper(Project, project_tbl,
id_attribute='project_id', slug_expression=project_slug,
properties=dict(customer=relationship(Customer, uselist=False)))
mapper(Site, site_tbl,
id_attribute='site_id', slug_expression=site_slug,
properties=dict(project=relationship(Project, uselist=False)))
mapper(Species, species_tbl,
id_attribute='species_id', slug_expression=species_slug)
mapper(Incidence, incidence_tbl,
slug_expression=incidence_slug,
properties=dict(species=relationship(Species, uselist=False),
site=relationship(Site, uselist=False)))
# Configure and initialize metadata.
metadata.bind = engine
metadata.create_all()
return metadata
|
The function first creates a database schema and then maps our entity classes to this schema. Note that a special mapper is used which provides a convenient way to map the special id and slug attributes required by everest to the ORM layer.
To use an engine other than the default in-memory SQLite database engine, you need to supply a db_string setting in the paster application .ini file. For example:
Different resorces may use different repositories, but any given resource can only be assigned to one repository.