Building everest applicationsΒΆ

In this section, you will find a step-by-step guide on how to build a RESTful application with everest.

  1. The application

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

  1. Designing the entity model

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
  1. Designing and building the resource layer

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.

  1. Configuring the application

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

  • A marker interface for your resource;
  • An entity class for the resource;
  • A member class class for the resource; and
  • A name for the root collection.

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.

  1. Running the application

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'
  1. Adding persistency

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.

Project Versions

Previous topic

everest Tutorial

Next topic

Using everest applications

This Page