diff options
author | Ryan Petrello <lists@ryanpetrello.com> | 2014-03-06 18:46:23 +0000 |
---|---|---|
committer | Ryan Petrello <lists@ryanpetrello.com> | 2014-03-06 20:46:47 +0000 |
commit | b046c1e965e2a37758c5092402a92b46217d58b4 (patch) | |
tree | 9852d38032471e1ca347d3911edfafbd23648883 | |
parent | 32712c50926ef27c58e1f16e14a4389d1a201f4b (diff) | |
download | pecan-b046c1e965e2a37758c5092402a92b46217d58b4.tar.gz |
Add a pecan scaffold for generating a simple REST API.
Change-Id: Iae346dc2d9a9cc52f3c8b4b546793c8d9670f56d
Fixes bug 1248822
19 files changed, 317 insertions, 8 deletions
diff --git a/MANIFEST.in b/MANIFEST.in index d708b6d..79e6393 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -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 diff --git a/pecan/scaffolds/__init__.py b/pecan/scaffolds/__init__.py index ac22464..2dbe46f 100644 --- a/pecan/scaffolds/__init__.py +++ b/pecan/scaffolds/__init__.py @@ -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 diff --git a/pecan/scaffolds/base/+package+/model/__init__.py b/pecan/scaffolds/base/+package+/model/__init__.py index 2f1740f..d983f7b 100644 --- a/pecan/scaffolds/base/+package+/model/__init__.py +++ b/pecan/scaffolds/base/+package+/model/__init__.py @@ -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. diff --git a/pecan/scaffolds/rest-api/+package+/__init__.py b/pecan/scaffolds/rest-api/+package+/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/pecan/scaffolds/rest-api/+package+/__init__.py diff --git a/pecan/scaffolds/rest-api/+package+/app.py_tmpl b/pecan/scaffolds/rest-api/+package+/app.py_tmpl new file mode 100644 index 0000000..3eb5edf --- /dev/null +++ b/pecan/scaffolds/rest-api/+package+/app.py_tmpl @@ -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 + ) diff --git a/pecan/scaffolds/rest-api/+package+/controllers/__init__.py b/pecan/scaffolds/rest-api/+package+/controllers/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/pecan/scaffolds/rest-api/+package+/controllers/__init__.py diff --git a/pecan/scaffolds/rest-api/+package+/controllers/root.py b/pecan/scaffolds/rest-api/+package+/controllers/root.py new file mode 100644 index 0000000..18ea5dc --- /dev/null +++ b/pecan/scaffolds/rest-api/+package+/controllers/root.py @@ -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!" diff --git a/pecan/scaffolds/rest-api/+package+/errors.py b/pecan/scaffolds/rest-api/+package+/errors.py new file mode 100644 index 0000000..4d4d06c --- /dev/null +++ b/pecan/scaffolds/rest-api/+package+/errors.py @@ -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' + ) diff --git a/pecan/scaffolds/rest-api/+package+/model/__init__.py b/pecan/scaffolds/rest-api/+package+/model/__init__.py new file mode 100644 index 0000000..d983f7b --- /dev/null +++ b/pecan/scaffolds/rest-api/+package+/model/__init__.py @@ -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 diff --git a/pecan/scaffolds/rest-api/+package+/tests/__init__.py_tmpl b/pecan/scaffolds/rest-api/+package+/tests/__init__.py_tmpl new file mode 100644 index 0000000..78ea527 --- /dev/null +++ b/pecan/scaffolds/rest-api/+package+/tests/__init__.py_tmpl @@ -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) diff --git a/pecan/scaffolds/rest-api/+package+/tests/config.py_tmpl b/pecan/scaffolds/rest-api/+package+/tests/config.py_tmpl new file mode 100644 index 0000000..09efcb7 --- /dev/null +++ b/pecan/scaffolds/rest-api/+package+/tests/config.py_tmpl @@ -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 diff --git a/pecan/scaffolds/rest-api/+package+/tests/test_functional.py_tmpl b/pecan/scaffolds/rest-api/+package+/tests/test_functional.py_tmpl new file mode 100644 index 0000000..f1ac12c --- /dev/null +++ b/pecan/scaffolds/rest-api/+package+/tests/test_functional.py_tmpl @@ -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.' + } diff --git a/pecan/scaffolds/rest-api/+package+/tests/test_units.py b/pecan/scaffolds/rest-api/+package+/tests/test_units.py new file mode 100644 index 0000000..573fb68 --- /dev/null +++ b/pecan/scaffolds/rest-api/+package+/tests/test_units.py @@ -0,0 +1,7 @@ +from unittest import TestCase + + +class TestUnits(TestCase): + + def test_units(self): + assert 5 * 5 == 25 diff --git a/pecan/scaffolds/rest-api/config.py_tmpl b/pecan/scaffolds/rest-api/config.py_tmpl new file mode 100644 index 0000000..bd4d29d --- /dev/null +++ b/pecan/scaffolds/rest-api/config.py_tmpl @@ -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 diff --git a/pecan/scaffolds/rest-api/setup.cfg_tmpl b/pecan/scaffolds/rest-api/setup.cfg_tmpl new file mode 100644 index 0000000..111f7cc --- /dev/null +++ b/pecan/scaffolds/rest-api/setup.cfg_tmpl @@ -0,0 +1,6 @@ +[nosetests] +match=^test +where=${package} +nocapture=1 +cover-package=${package} +cover-erase=1 diff --git a/pecan/scaffolds/rest-api/setup.py_tmpl b/pecan/scaffolds/rest-api/setup.py_tmpl new file mode 100644 index 0000000..ec47896 --- /dev/null +++ b/pecan/scaffolds/rest-api/setup.py_tmpl @@ -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']) +) diff --git a/pecan/tests/scaffold_builder.py b/pecan/tests/scaffold_builder.py index 563e762..2c403ae 100644 --- a/pecan/tests/scaffold_builder.py +++ b/pecan/tests/scaffold_builder.py @@ -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: @@ -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 @@ -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 |