Remove cornice integration
As with turbogears, no one was using this in OpenStack and therefore we can and should remove this adaptor. Change-Id: I0d3942680c1156e57d70f334caea6b89590b46c7 Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
This commit is contained in:
parent
a9faca002b
commit
21dadaef0e
@ -62,7 +62,7 @@ Main features
|
||||
- Extensible : easy to add more protocols or more base types.
|
||||
- Framework independence : adapters are provided to easily integrate
|
||||
your API in any web framework, for example a wsgi container,
|
||||
Pecan_, Flask_, cornice_...
|
||||
Pecan_, Flask_, ...
|
||||
- Very few runtime dependencies: webob, simplegeneric. Optionnaly lxml and
|
||||
simplejson if you need better performances.
|
||||
- Integration in `Sphinx`_ for making clean documentation with
|
||||
@ -70,7 +70,6 @@ Main features
|
||||
|
||||
.. _Pecan: http://pecanpy.org/
|
||||
.. _Flask: http://flask.pocoo.org/
|
||||
.. _cornice: http://pypi.python.org/pypi/cornice
|
||||
|
||||
Install
|
||||
~~~~~~~
|
||||
|
@ -5,6 +5,7 @@ Changes
|
||||
--------------
|
||||
|
||||
* Remove support for turbogears
|
||||
* Remove support for cornice
|
||||
* Remove SQLAlchemy support. It has never actually worked to begin with.
|
||||
|
||||
0.9.2 (2017-02-14)
|
||||
|
@ -14,22 +14,22 @@ The decorators
|
||||
Depending on the framework you are using, you will have to use either a
|
||||
@\ :class:`wsme.signature` decorator or a @\ :class:`wsme.wsexpose` decorator.
|
||||
|
||||
@signature
|
||||
@signature
|
||||
~~~~~~~~~~
|
||||
|
||||
The base @\ :class:`wsme.signature` decorator defines the return and argument types
|
||||
of the function, and if needed a few more options.
|
||||
|
||||
The Flask and Cornice adapters both propose a specific version of it, which
|
||||
also wrap the function so that it becomes suitable for the host framework.
|
||||
The Flask and adapter proposes a specific version of it, which also wrap the
|
||||
function so that it becomes suitable for the host framework.
|
||||
|
||||
In any case, the use of @\ :class:`wsme.signature` has the same meaning: tell WSME what is the
|
||||
signature of the function.
|
||||
In any case, the use of @\ :class:`wsme.signature` has the same meaning: tell
|
||||
WSME what is the signature of the function.
|
||||
|
||||
@wsexpose
|
||||
~~~~~~~~~
|
||||
|
||||
The native Rest implementation, and the TG and Pecan adapters add a @\ :class:`wsme.wsexpose`
|
||||
The native Rest implementation, and the Pecan adapter add a @\ :class:`wsme.wsexpose`
|
||||
decorator.
|
||||
|
||||
It does what @\ :class:`wsme.signature` does, *and* exposes the function in the routing system
|
||||
|
@ -24,8 +24,7 @@ This decorator can have two different names depending on the adapter.
|
||||
|
||||
Generally this decorator is provided for frameworks that expect functions
|
||||
taking a request object as a single parameter and returning a response
|
||||
object. This is the case for :ref:`adapter-cornice` and
|
||||
:ref:`adapter-flask`.
|
||||
object. This is the case for :ref:`adapter-flask`.
|
||||
|
||||
If you want to enable additional protocols, you will need to
|
||||
mount a :class:`WSRoot` instance somewhere in the application, generally
|
||||
@ -62,73 +61,6 @@ WSME, which is the case if you write a WSME standalone application.
|
||||
application = root.wsgiapp()
|
||||
|
||||
|
||||
.. _adapter-cornice:
|
||||
|
||||
Cornice
|
||||
-------
|
||||
|
||||
.. _cornice: http://cornice.readthedocs.org/en/latest/
|
||||
|
||||
*"* Cornice_ *provides helpers to build & document REST-ish Web Services with
|
||||
Pyramid, with decent default behaviors. It takes care of following the HTTP
|
||||
specification in an automated way where possible."*
|
||||
|
||||
|
||||
:mod:`wsmeext.cornice` -- Cornice adapter
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. module:: wsmeext.cornice
|
||||
|
||||
.. function:: signature
|
||||
|
||||
Declare the parameters of a function and returns a function suitable for
|
||||
cornice (ie that takes a request and returns a response).
|
||||
|
||||
Configuration
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
To use WSME with Cornice you have to add a configuration option to your Pyramid application.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pyramid.config import Configurator
|
||||
|
||||
|
||||
def make_app():
|
||||
config = Configurator()
|
||||
config.include("cornice")
|
||||
config.include("wsmeext.cornice") # This includes WSME cornice support
|
||||
# ...
|
||||
return config.make_wsgi_app()
|
||||
|
||||
Example
|
||||
~~~~~~~
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from cornice import Service
|
||||
from wsmeext.cornice import signature
|
||||
import wsme.types
|
||||
|
||||
hello = Service(name='hello', path='/', description="Simplest app")
|
||||
|
||||
class Info(wsme.types.Base):
|
||||
message = wsme.types.text
|
||||
|
||||
|
||||
@hello.get()
|
||||
@signature(Info)
|
||||
def get_info():
|
||||
"""Returns Hello in JSON or XML."""
|
||||
return Info(message='Hello World')
|
||||
|
||||
|
||||
@hello.post()
|
||||
@signature(None, Info)
|
||||
def set_info(info):
|
||||
print("Got a message: %s" % info.message)
|
||||
|
||||
|
||||
.. _adapter-flask:
|
||||
|
||||
Flask
|
||||
@ -224,7 +156,7 @@ The `example <http://pecan.readthedocs.org/en/latest/rest.html#nesting-restcontr
|
||||
.. code-block:: python
|
||||
|
||||
from wsmeext.pecan import wsexpose
|
||||
|
||||
|
||||
class BooksController(RestController):
|
||||
@wsexpose(Book, int, int)
|
||||
def get(self, author_id, id):
|
||||
@ -238,37 +170,3 @@ The `example <http://pecan.readthedocs.org/en/latest/rest.html#nesting-restcontr
|
||||
books = BooksController()
|
||||
|
||||
.. _Pecan: http://pecanpy.org/
|
||||
|
||||
.. _adapter-tg1:
|
||||
|
||||
Other frameworks
|
||||
----------------
|
||||
|
||||
Bottle
|
||||
~~~~~~
|
||||
|
||||
No adapter is provided yet but it should not be hard to write one, by taking
|
||||
example on the cornice adapter.
|
||||
|
||||
This example only show how to mount a WSRoot inside a bottle application.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import bottle
|
||||
import wsme
|
||||
|
||||
class MyRoot(wsme.WSRoot):
|
||||
@wsme.expose(unicode)
|
||||
def helloworld(self):
|
||||
return u"Hello World !"
|
||||
|
||||
root = MyRoot(webpath='/ws', protocols=['restjson'])
|
||||
|
||||
bottle.mount('/ws', root.wsgiapp())
|
||||
bottle.run()
|
||||
|
||||
Pyramid
|
||||
~~~~~~~
|
||||
|
||||
The recommended way of using WSME inside Pyramid is to use
|
||||
:ref:`adapter-cornice`.
|
||||
|
@ -1,183 +0,0 @@
|
||||
import unittest
|
||||
import json
|
||||
|
||||
import webtest
|
||||
|
||||
from cornice import Service
|
||||
from cornice import resource
|
||||
from pyramid.config import Configurator
|
||||
from pyramid.httpexceptions import HTTPUnauthorized
|
||||
|
||||
from wsme.types import text, Base, HostRequest
|
||||
from wsmeext.cornice import signature
|
||||
|
||||
|
||||
class User(Base):
|
||||
id = int
|
||||
name = text
|
||||
|
||||
users = Service(name='users', path='/users')
|
||||
|
||||
|
||||
@users.get()
|
||||
@signature([User])
|
||||
def users_get():
|
||||
return [User(id=1, name='first')]
|
||||
|
||||
|
||||
@users.post()
|
||||
@signature(User, body=User)
|
||||
def users_create(data):
|
||||
data.id = 2
|
||||
return data
|
||||
|
||||
|
||||
secret = Service(name='secrets', path='/secret')
|
||||
|
||||
|
||||
@secret.get()
|
||||
@signature()
|
||||
def secret_get():
|
||||
raise HTTPUnauthorized()
|
||||
|
||||
|
||||
divide = Service(name='divide', path='/divide')
|
||||
|
||||
|
||||
@divide.get()
|
||||
@signature(int, int, int)
|
||||
def do_divide(a, b):
|
||||
return a / b
|
||||
|
||||
needrequest = Service(name='needrequest', path='/needrequest')
|
||||
|
||||
|
||||
@needrequest.get()
|
||||
@signature(bool, HostRequest)
|
||||
def needrequest_get(request):
|
||||
assert request.path == '/needrequest', request.path
|
||||
return True
|
||||
|
||||
|
||||
class Author(Base):
|
||||
authorId = int
|
||||
name = text
|
||||
|
||||
|
||||
@resource.resource(collection_path='/author', path='/author/{authorId}')
|
||||
class AuthorResource(object):
|
||||
def __init__(self, request):
|
||||
self.request = request
|
||||
|
||||
@signature(Author, int)
|
||||
def get(self, authorId):
|
||||
return Author(authorId=authorId, name="Author %s" % authorId)
|
||||
|
||||
@signature(Author, int, body=Author)
|
||||
def post(self, authorId, data):
|
||||
data.authorId = authorId
|
||||
return data
|
||||
|
||||
@signature([Author], text)
|
||||
def collection_get(self, where=None):
|
||||
return [
|
||||
Author(authorId=1, name="Author 1"),
|
||||
Author(authorId=2, name="Author 2"),
|
||||
Author(authorId=3, name="Author 3")
|
||||
]
|
||||
|
||||
|
||||
def make_app():
|
||||
config = Configurator()
|
||||
config.include("cornice")
|
||||
config.include("wsmeext.cornice")
|
||||
config.scan("test_cornice")
|
||||
return config.make_wsgi_app()
|
||||
|
||||
|
||||
class WSMECorniceTestCase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.app = webtest.TestApp(make_app())
|
||||
|
||||
def test_get_json_list(self):
|
||||
resp = self.app.get('/users')
|
||||
self.assertEqual(
|
||||
resp.body,
|
||||
b'[{"id": 1, "name": "first"}]'
|
||||
)
|
||||
|
||||
def test_get_xml_list(self):
|
||||
resp = self.app.get('/users', headers={"Accept": "text/xml"})
|
||||
self.assertEqual(
|
||||
resp.body,
|
||||
b'<result><item><id>1</id><name>first</name></item></result>'
|
||||
)
|
||||
|
||||
def test_post_json_data(self):
|
||||
data = json.dumps({"name": "new"})
|
||||
resp = self.app.post(
|
||||
'/users', data,
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
self.assertEqual(
|
||||
resp.body,
|
||||
b'{"id": 2, "name": "new"}'
|
||||
)
|
||||
|
||||
def test_post_xml_data(self):
|
||||
data = '<data><name>new</name></data>'
|
||||
resp = self.app.post(
|
||||
'/users', data,
|
||||
headers={"Content-Type": "text/xml"}
|
||||
)
|
||||
self.assertEqual(
|
||||
resp.body,
|
||||
b'<result><id>2</id><name>new</name></result>'
|
||||
)
|
||||
|
||||
def test_pass_request(self):
|
||||
resp = self.app.get('/needrequest')
|
||||
assert resp.json is True
|
||||
|
||||
def test_resource_collection_get(self):
|
||||
resp = self.app.get('/author')
|
||||
assert len(resp.json) == 3
|
||||
assert resp.json[0]['name'] == 'Author 1'
|
||||
assert resp.json[1]['name'] == 'Author 2'
|
||||
assert resp.json[2]['name'] == 'Author 3'
|
||||
|
||||
def test_resource_get(self):
|
||||
resp = self.app.get('/author/5')
|
||||
assert resp.json['name'] == 'Author 5'
|
||||
|
||||
def test_resource_post(self):
|
||||
resp = self.app.post(
|
||||
'/author/5',
|
||||
json.dumps({"name": "Author 5"}),
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
assert resp.json['authorId'] == 5
|
||||
assert resp.json['name'] == 'Author 5'
|
||||
|
||||
def test_server_error(self):
|
||||
resp = self.app.get('/divide?a=1&b=0', expect_errors=True)
|
||||
self.assertEqual(resp.json['faultcode'], 'Server')
|
||||
self.assertEqual(resp.status_code, 500)
|
||||
|
||||
def test_client_error(self):
|
||||
resp = self.app.get(
|
||||
'/divide?a=1&c=0',
|
||||
headers={'Accept': 'application/json'},
|
||||
expect_errors=True
|
||||
)
|
||||
self.assertEqual(resp.json['faultcode'], 'Client')
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
|
||||
def test_runtime_error(self):
|
||||
resp = self.app.get(
|
||||
'/secret',
|
||||
headers={'Accept': 'application/json'},
|
||||
expect_errors=True
|
||||
)
|
||||
self.assertEqual(resp.json['faultcode'], 'Client')
|
||||
self.assertEqual(resp.status_code, 401)
|
25
tox.ini
25
tox.ini
@ -1,5 +1,5 @@
|
||||
[tox]
|
||||
envlist = py27,py27-nolxml,pypy,cornice,cornice-py3,coverage,py36,py35,py36-nolxml,py35-nolxml,pecan-dev27,pecan-dev35,pecan-dev36,pep8
|
||||
envlist = py27,py27-nolxml,pypy,coverage,py36,py35,py36-nolxml,py35-nolxml,pecan-dev27,pecan-dev35,pecan-dev36,pep8
|
||||
|
||||
[common]
|
||||
testtools =
|
||||
@ -20,29 +20,6 @@ basedeps =
|
||||
setenv =
|
||||
COVERAGE_FILE=.coverage.{envname}
|
||||
|
||||
[testenv:cornice]
|
||||
basepython = python2.7
|
||||
usedevelop = True
|
||||
deps =
|
||||
pbr
|
||||
nose
|
||||
webtest
|
||||
coverage < 3.99
|
||||
cornice
|
||||
commands =
|
||||
{envbindir}/nosetests tests/test_cornice.py --with-xunit --xunit-file nosetests-{envname}.xml --verbose --with-coverage --cover-package wsmeext {posargs}
|
||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsmeext/cornice.py
|
||||
|
||||
[testenv:cornice-py3]
|
||||
basepython = python3.6
|
||||
usedevelop = {[testenv:cornice]usedevelop}
|
||||
deps = {[testenv:cornice]deps}
|
||||
setenv =
|
||||
PYTHONHASHSEED=0
|
||||
commands =
|
||||
{envbindir}/nosetests tests/test_cornice.py --with-xunit --xunit-file nosetests-{envname}.xml --verbose --with-coverage --cover-package wsmeext {posargs}
|
||||
{envbindir}/coverage xml -o coverage-{envname}.xml wsme/*.py wsmeext/cornice.py
|
||||
|
||||
[testenv:pecan-dev-base]
|
||||
deps =
|
||||
{[common]testtools}
|
||||
|
@ -1,168 +0,0 @@
|
||||
"""
|
||||
WSME for cornice
|
||||
|
||||
|
||||
Activate it::
|
||||
|
||||
config.include('wsme.cornice')
|
||||
|
||||
|
||||
And use it::
|
||||
|
||||
@hello.get()
|
||||
@wsexpose(Message, wsme.types.text)
|
||||
def get_hello(who=u'World'):
|
||||
return Message(text='Hello %s' % who)
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
import inspect
|
||||
import sys
|
||||
|
||||
import wsme
|
||||
from wsme.rest import json as restjson
|
||||
from wsme.rest import xml as restxml
|
||||
import wsme.runtime
|
||||
import wsme.api
|
||||
import functools
|
||||
|
||||
from wsme.rest.args import (
|
||||
args_from_args, args_from_params, args_from_body, combine_args
|
||||
)
|
||||
|
||||
|
||||
class WSMEJsonRenderer(object):
|
||||
def __init__(self, info):
|
||||
pass
|
||||
|
||||
def __call__(self, data, context):
|
||||
response = context['request'].response
|
||||
response.content_type = 'application/json'
|
||||
if 'faultcode' in data:
|
||||
if 'orig_code' in data:
|
||||
response.status_code = data['orig_code']
|
||||
elif data['faultcode'] == 'Client':
|
||||
response.status_code = 400
|
||||
else:
|
||||
response.status_code = 500
|
||||
return restjson.encode_error(None, data)
|
||||
obj = data['result']
|
||||
if isinstance(obj, wsme.api.Response):
|
||||
response.status_code = obj.status_code
|
||||
if obj.error:
|
||||
return restjson.encode_error(None, obj.error)
|
||||
obj = obj.obj
|
||||
return restjson.encode_result(obj, data['datatype'])
|
||||
|
||||
|
||||
class WSMEXmlRenderer(object):
|
||||
def __init__(self, info):
|
||||
pass
|
||||
|
||||
def __call__(self, data, context):
|
||||
response = context['request'].response
|
||||
if 'faultcode' in data:
|
||||
if data['faultcode'] == 'Client':
|
||||
response.status_code = 400
|
||||
else:
|
||||
response.status_code = 500
|
||||
return restxml.encode_error(None, data)
|
||||
response.content_type = 'text/xml'
|
||||
return restxml.encode_result(data['result'], data['datatype'])
|
||||
|
||||
|
||||
def get_outputformat(request):
|
||||
df = None
|
||||
if 'Accept' in request.headers:
|
||||
if 'application/json' in request.headers['Accept']:
|
||||
df = 'json'
|
||||
elif 'text/xml' in request.headers['Accept']:
|
||||
df = 'xml'
|
||||
if df is None and 'Content-Type' in request.headers:
|
||||
if 'application/json' in request.headers['Content-Type']:
|
||||
df = 'json'
|
||||
elif 'text/xml' in request.headers['Content-Type']:
|
||||
df = 'xml'
|
||||
return df if df else 'json'
|
||||
|
||||
|
||||
def signature(*args, **kwargs):
|
||||
sig = wsme.signature(*args, **kwargs)
|
||||
|
||||
def decorate(f):
|
||||
args = inspect.getargspec(f)[0]
|
||||
with_self = args[0] == 'self' if args else False
|
||||
f = sig(f)
|
||||
funcdef = wsme.api.FunctionDefinition.get(f)
|
||||
funcdef.resolve_types(wsme.types.registry)
|
||||
|
||||
@functools.wraps(f)
|
||||
def callfunction(*args):
|
||||
if with_self:
|
||||
if len(args) == 1:
|
||||
self = args[0]
|
||||
request = self.request
|
||||
elif len(args) == 2:
|
||||
self, request = args
|
||||
else:
|
||||
raise ValueError("Cannot do anything with these arguments")
|
||||
else:
|
||||
request = args[0]
|
||||
request.override_renderer = 'wsme' + get_outputformat(request)
|
||||
try:
|
||||
args, kwargs = combine_args(funcdef, (
|
||||
args_from_args(funcdef, (), request.matchdict),
|
||||
args_from_params(funcdef, request.params),
|
||||
args_from_body(funcdef, request.body, request.content_type)
|
||||
))
|
||||
wsme.runtime.check_arguments(funcdef, args, kwargs)
|
||||
if funcdef.pass_request:
|
||||
kwargs[funcdef.pass_request] = request
|
||||
if with_self:
|
||||
args.insert(0, self)
|
||||
|
||||
result = f(*args, **kwargs)
|
||||
return {
|
||||
'datatype': funcdef.return_type,
|
||||
'result': result
|
||||
}
|
||||
except Exception:
|
||||
try:
|
||||
exception_info = sys.exc_info()
|
||||
orig_exception = exception_info[1]
|
||||
orig_code = getattr(orig_exception, 'code', None)
|
||||
data = wsme.api.format_exception(exception_info)
|
||||
if orig_code is not None:
|
||||
data['orig_code'] = orig_code
|
||||
return data
|
||||
finally:
|
||||
del exception_info
|
||||
|
||||
callfunction.wsme_func = f
|
||||
return callfunction
|
||||
return decorate
|
||||
|
||||
|
||||
def scan_api(root=None):
|
||||
from cornice.service import get_services
|
||||
for service in get_services():
|
||||
for method, func, options in service.definitions:
|
||||
wsme_func = getattr(func, 'wsme_func')
|
||||
basepath = service.path.split('/')
|
||||
if basepath and not basepath[0]:
|
||||
del basepath[0]
|
||||
if wsme_func:
|
||||
yield (
|
||||
basepath + [method.lower()],
|
||||
wsme_func._wsme_definition
|
||||
)
|
||||
|
||||
|
||||
def includeme(config):
|
||||
import pyramid.wsgi
|
||||
wsroot = wsme.WSRoot(scan_api=scan_api, webpath='/ws')
|
||||
wsroot.addprotocol('extdirect')
|
||||
config.add_renderer('wsmejson', WSMEJsonRenderer)
|
||||
config.add_renderer('wsmexml', WSMEXmlRenderer)
|
||||
config.add_route('wsme', '/ws/*path')
|
||||
config.add_view(pyramid.wsgi.wsgiapp(wsroot.wsgiapp()), route_name='wsme')
|
Loading…
x
Reference in New Issue
Block a user