Add a pecan scaffold for generating a simple REST API.

Change-Id: Iae346dc2d9a9cc52f3c8b4b546793c8d9670f56d
Fixes bug 1248822
This commit is contained in:
Ryan Petrello
2014-03-06 18:46:23 +00:00
parent 32712c5092
commit b046c1e965
19 changed files with 317 additions and 8 deletions

View File

@@ -1,4 +1,6 @@
recursive-include pecan/scaffolds/base *
include pecan/scaffolds/base/*
recursive-include pecan/scaffolds/rest-api *
include pecan/scaffolds/rest-api/*
include pecan/middleware/resources/*
include LICENSE README.rst requirements.txt

View File

@@ -42,6 +42,10 @@ class BaseScaffold(PecanScaffold):
_scaffold_dir = ('pecan', os.path.join('scaffolds', 'base'))
class RestAPIScaffold(PecanScaffold):
_scaffold_dir = ('pecan', os.path.join('scaffolds', 'rest-api'))
def copy_dir(source, dest, variables, out_=sys.stdout, i=0):
"""
Copies the ``source`` directory to the ``dest`` directory, where

View File

@@ -5,7 +5,7 @@ def init_model():
"""
This is a stub method which is called at application startup time.
If you need to bind to a parse database configuration, set up tables or
If you need to bind to a parsed database configuration, set up tables or
ORM classes, or perform any database initialization, this is the
recommended place to do it.

View File

@@ -0,0 +1,16 @@
from pecan import make_app
from ${package} import model
from ${package}.errors import JSONErrorHook
def setup_app(config):
model.init_model()
app_conf = dict(config.app)
return make_app(
app_conf.pop('root'),
logging=getattr(config, 'logging', {}),
hooks=[JSONErrorHook()],
**app_conf
)

View File

@@ -0,0 +1,44 @@
from pecan import expose, response, abort
from pecan.rest import RestController
people = {
1: 'Luke',
2: 'Leia',
3: 'Han',
4: 'Anakin'
}
class PeopleController(RestController):
@expose('json')
def get_all(self):
return people
@expose()
def get_one(self, person_id):
return people.get(int(person_id)) or abort(404)
@expose()
def post(self):
# TODO: Create a new person
response.status = 201
@expose()
def put(self, person_id):
# TODO: Idempotent PUT (returns 200 or 204)
response.status = 204
@expose()
def delete(self, person_id):
# TODO: Idempotent DELETE
response.status = 200
class RootController(object):
people = PeopleController()
@expose()
def index(self):
return "Hello, World!"

View File

@@ -0,0 +1,18 @@
import json
import webob
from pecan.hooks import PecanHook
class JSONErrorHook(PecanHook):
"""
A pecan hook that translates webob HTTP errors into a JSON format.
"""
def on_error(self, state, exc):
if isinstance(exc, webob.exc.HTTPError):
return webob.Response(
body=json.dumps({'reason': str(exc)}),
status=exc.status,
headerlist=exc.headerlist,
content_type='application/json'
)

View File

@@ -0,0 +1,15 @@
from pecan import conf # noqa
def init_model():
"""
This is a stub method which is called at application startup time.
If you need to bind to a parsed database configuration, set up tables or
ORM classes, or perform any database initialization, this is the
recommended place to do it.
For more information working with databases, and some common recipes,
see http://pecan.readthedocs.org/en/latest/databases.html
"""
pass

View File

@@ -0,0 +1,22 @@
import os
from unittest import TestCase
from pecan import set_config
from pecan.testing import load_test_app
__all__ = ['FunctionalTest']
class FunctionalTest(TestCase):
"""
Used for functional tests where you need to test your
literal application and its integration with the framework.
"""
def setUp(self):
self.app = load_test_app(os.path.join(
os.path.dirname(__file__),
'config.py'
))
def tearDown(self):
set_config({}, overwrite=True)

View File

@@ -0,0 +1,19 @@
# Server Specific Configurations
server = {
'port': '8080',
'host': '0.0.0.0'
}
# Pecan Application Configurations
app = {
'root': '${package}.controllers.root.RootController',
'modules': ['${package}'],
'debug': True
}
# Custom Configurations must be in Python dictionary format::
#
# foo = {'bar':'baz'}
#
# All configurations are accessible at::
# pecan.conf

View File

@@ -0,0 +1,37 @@
import json
from ${package}.tests import FunctionalTest
class TestRootController(FunctionalTest):
def test_get_all(self):
response = self.app.get('/people/')
assert response.status_int == 200
assert response.namespace[1] == 'Luke'
assert response.namespace[2] == 'Leia'
assert response.namespace[3] == 'Han'
assert response.namespace[4] == 'Anakin'
def test_get_one(self):
response = self.app.get('/people/1/')
assert response.status_int == 200
assert response.body.decode() == 'Luke'
def test_post(self):
response = self.app.post('/people/')
assert response.status_int == 201
def test_put(self):
response = self.app.put('/people/1')
assert response.status_int == 204
def test_delete(self):
response = self.app.delete('/people/1')
assert response.status_int == 200
def test_not_found(self):
response = self.app.get('/missing/', expect_errors=True)
assert response.status_int == 404
assert json.loads(response.body.decode()) == {
'reason': 'The resource could not be found.'
}

View File

@@ -0,0 +1,7 @@
from unittest import TestCase
class TestUnits(TestCase):
def test_units(self):
assert 5 * 5 == 25

View File

@@ -0,0 +1,41 @@
# Server Specific Configurations
server = {
'port': '8080',
'host': '0.0.0.0'
}
# Pecan Application Configurations
app = {
'root': '${package}.controllers.root.RootController',
'modules': ['${package}'],
'debug': True
}
logging = {
'loggers': {
'root': {'level': 'INFO', 'handlers': ['console']},
'${package}': {'level': 'DEBUG', 'handlers': ['console']},
'py.warnings': {'handlers': ['console']},
'__force_dict__': True
},
'handlers': {
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'formatter': 'simple'
}
},
'formatters': {
'simple': {
'format': ('%(asctime)s %(levelname)-5.5s [%(name)s]'
'[%(threadName)s] %(message)s')
}
}
}
# Custom Configurations must be in Python dictionary format::
#
# foo = {'bar':'baz'}
#
# All configurations are accessible at::
# pecan.conf

View File

@@ -0,0 +1,6 @@
[nosetests]
match=^test
where=${package}
nocapture=1
cover-package=${package}
cover-erase=1

View File

@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
try:
from setuptools import setup, find_packages
except ImportError:
from ez_setup import use_setuptools
use_setuptools()
from setuptools import setup, find_packages
setup(
name='${package}',
version='0.1',
description='',
author='',
author_email='',
install_requires=[
"pecan",
],
test_suite='${package}',
zip_safe=False,
include_package_data=True,
packages=find_packages(exclude=['ez_setup'])
)

View File

@@ -58,9 +58,8 @@ if __name__ == '__main__':
try:
# ...and that it's serving (valid) content...
resp = urlopen('http://localhost:8080/')
assert resp.getcode() == 200
assert 'This is a sample Pecan project.' in \
resp.read().decode()
assert resp.getcode()
assert len(resp.read().decode())
except URLError:
pass
else:
@@ -112,9 +111,8 @@ if __name__ == '__main__':
try:
# ...and that it's serving (valid) content...
resp = urlopen('http://localhost:%d/' % port)
assert resp.getcode() == 200
assert 'This is a sample Pecan project.' in \
resp.read().decode()
assert resp.getcode()
assert len(resp.read().decode())
except URLError:
pass
else:

View File

@@ -118,6 +118,7 @@ setup(
create = pecan.commands:CreateCommand
[pecan.scaffold]
base = pecan.scaffolds:BaseScaffold
rest-api = pecan.scaffolds:RestAPIScaffold
[console_scripts]
pecan = pecan.commands:CommandRunner.handle_command_line
gunicorn_pecan = pecan.commands.serve:gunicorn_run

59
tox.ini
View File

@@ -1,5 +1,5 @@
[tox]
envlist = py26,py27,py32,py33,py34,scaffolds-26,scaffolds-27,scaffolds-32,scaffolds-33,scaffolds-34,pep8
envlist = py26,py27,py32,py33,py34,scaffolds-26,scaffolds-27,scaffolds-32,scaffolds-33,scaffolds-34,scaffolds-26-rest-api,scaffolds-27-rest-api,scaffolds-32-rest-api,scaffolds-33-rest-api,scaffolds-34-rest-api,pep8
[testenv]
commands={envpython} setup.py test -v
@@ -20,6 +20,17 @@ commands=pecan create testing123
pep8 --repeat --show-source testing123/setup.py testing123/testing123
{envpython} {toxinidir}/pecan/tests/scaffold_builder.py
[testenv:scaffolds-26-rest-api]
basepython = python2.6
deps = {[testenv:scaffolds-base]deps}
unittest2
changedir={envdir}/tmp
commands=pecan create testing123 rest-api
{envpython} testing123/setup.py install
{envpython} testing123/setup.py test -q
pep8 --repeat --show-source testing123/setup.py testing123/testing123
{envpython} {toxinidir}/pecan/tests/scaffold_builder.py
[testenv:scaffolds-27]
basepython = python2.7
deps = {[testenv:scaffolds-base]deps}
@@ -30,6 +41,16 @@ commands=pecan create testing123
pep8 --repeat --show-source testing123/setup.py testing123/testing123
{envpython} {toxinidir}/pecan/tests/scaffold_builder.py
[testenv:scaffolds-27-rest-api]
basepython = python2.7
deps = {[testenv:scaffolds-base]deps}
changedir={[testenv:scaffolds-26]changedir}
commands=pecan create testing123 rest-api
{envpython} testing123/setup.py install
{envpython} testing123/setup.py test -q
pep8 --repeat --show-source testing123/setup.py testing123/testing123
{envpython} {toxinidir}/pecan/tests/scaffold_builder.py
[testenv:scaffolds-32]
basepython = python3.2
deps = {[testenv:scaffolds-base]deps}
@@ -42,6 +63,18 @@ commands=pecan create testing123
pep8 --repeat --show-source testing123/setup.py testing123/testing123
{envpython} {toxinidir}/pecan/tests/scaffold_builder.py
[testenv:scaffolds-32-rest-api]
basepython = python3.2
deps = {[testenv:scaffolds-base]deps}
changedir={[testenv:scaffolds-26]changedir}
commands=pecan create testing123 rest-api
curl "http://python-distribute.org/distribute_setup.py" -O
{envpython} distribute_setup.py
{envpython} testing123/setup.py install
{envpython} testing123/setup.py test -q
pep8 --repeat --show-source testing123/setup.py testing123/testing123
{envpython} {toxinidir}/pecan/tests/scaffold_builder.py
[testenv:scaffolds-33]
basepython = python3.3
deps = {[testenv:scaffolds-base]deps}
@@ -54,6 +87,18 @@ commands=pecan create testing123
pep8 --repeat --show-source testing123/setup.py testing123/testing123
{envpython} {toxinidir}/pecan/tests/scaffold_builder.py
[testenv:scaffolds-33-rest-api]
basepython = python3.3
deps = {[testenv:scaffolds-base]deps}
changedir={[testenv:scaffolds-26]changedir}
commands=pecan create testing123 rest-api
curl "http://python-distribute.org/distribute_setup.py" -O
{envpython} distribute_setup.py
{envpython} testing123/setup.py install
{envpython} testing123/setup.py test -q
pep8 --repeat --show-source testing123/setup.py testing123/testing123
{envpython} {toxinidir}/pecan/tests/scaffold_builder.py
[testenv:scaffolds-34]
basepython = python3.4
deps = {[testenv:scaffolds-base]deps}
@@ -66,6 +111,18 @@ commands=pecan create testing123
pep8 --repeat --show-source testing123/setup.py testing123/testing123
{envpython} {toxinidir}/pecan/tests/scaffold_builder.py
[testenv:scaffolds-34-rest-api]
basepython = python3.4
deps = {[testenv:scaffolds-base]deps}
changedir={[testenv:scaffolds-26]changedir}
commands=pecan create testing123 rest-api
curl "http://python-distribute.org/distribute_setup.py" -O
{envpython} distribute_setup.py
{envpython} testing123/setup.py install
{envpython} testing123/setup.py test -q
pep8 --repeat --show-source testing123/setup.py testing123/testing123
{envpython} {toxinidir}/pecan/tests/scaffold_builder.py
[testenv:wsme-stable]
basepython = python2.7
deps = nose