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:
Stephen Finucane 2019-05-01 10:32:38 -06:00
parent a9faca002b
commit 21dadaef0e
7 changed files with 11 additions and 487 deletions

View File

@ -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
~~~~~~~

View File

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

View File

@ -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

View File

@ -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`.

View File

@ -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
View File

@ -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}

View File

@ -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')