├── setup.cfg ├── txrestapi ├── __init__.py ├── service.py ├── methods.py ├── resource.py ├── json_resource.py └── tests.py ├── .gitignore ├── setup.py ├── LICENSE └── README.rst /setup.cfg: -------------------------------------------------------------------------------- 1 | [egg_info] 2 | -------------------------------------------------------------------------------- /txrestapi/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _trial_temp 2 | txrestapi.egg-info 3 | txrestapi/_trial_temp 4 | -------------------------------------------------------------------------------- /txrestapi/service.py: -------------------------------------------------------------------------------- 1 | from twisted.web.server import Site 2 | from .resource import APIResource 3 | 4 | 5 | class RESTfulService(Site): 6 | def __init__(self, port=8080): 7 | self.root = APIResource() 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | import sys, os 3 | 4 | version = '0.2' 5 | 6 | setup(name='txrestapi', 7 | version=version, 8 | description="Easing the creation of REST API services in Python", 9 | long_description="""\ 10 | """, 11 | classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers 12 | keywords='', 13 | author='Ian McCracken', 14 | author_email='ian.mccracken@gmail.com', 15 | url='http://github.com/iancmcc/txrestapi', 16 | license='MIT', 17 | packages=find_packages(exclude=['ez_setup', 'examples', 'tests']), 18 | include_package_data=True, 19 | zip_safe=False, 20 | install_requires=[ 21 | # -*- Extra requirements: -*- 22 | ], 23 | entry_points=""" 24 | # -*- Entry points: -*- 25 | """, 26 | ) 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Ian McCracken 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /txrestapi/methods.py: -------------------------------------------------------------------------------- 1 | from six import PY2, b 2 | from zope.interface.advice import addClassAdvisor 3 | 4 | def method_factory_factory(method): 5 | def factory_py2(regex): 6 | _f = {} 7 | def decorator(f): 8 | _f[f.__name__] = f 9 | return f 10 | def advisor(cls): 11 | def wrapped(f): 12 | def __init__(self, *args, **kwargs): 13 | f(self, *args, **kwargs) 14 | for func_name in _f: 15 | orig = _f[func_name] 16 | func = getattr(self, func_name) 17 | if func.im_func==orig: 18 | self.register(method, regex, func) 19 | return __init__ 20 | cls.__init__ = wrapped(cls.__init__) 21 | return cls 22 | addClassAdvisor(advisor) 23 | return decorator 24 | 25 | def factory_py3(regex): 26 | 27 | def decorator(f): 28 | current_methods = getattr(f, '__txrestapi__', []) 29 | current_methods.append((method, regex, )) 30 | f.__txrestapi__ = current_methods 31 | return f 32 | 33 | return decorator 34 | 35 | factory = factory_py2 if PY2 else factory_py3 36 | return factory 37 | 38 | ALL = method_factory_factory(b('ALL')) 39 | GET = method_factory_factory(b('GET')) 40 | POST = method_factory_factory(b('POST')) 41 | PUT = method_factory_factory(b('PUT')) 42 | DELETE = method_factory_factory(b('DELETE')) 43 | -------------------------------------------------------------------------------- /txrestapi/resource.py: -------------------------------------------------------------------------------- 1 | import re 2 | from six import PY2, PY3, b, u 3 | if PY2: 4 | from itertools import ifilter as filter 5 | from functools import wraps 6 | from twisted.web.resource import Resource, NoResource 7 | 8 | class _FakeResource(Resource): 9 | _result = '' 10 | isLeaf = True 11 | def __init__(self, result): 12 | Resource.__init__(self) 13 | self._result = result 14 | def render(self, request): 15 | return self._result 16 | 17 | 18 | def maybeResource(f): 19 | @wraps(f) 20 | def inner(*args, **kwargs): 21 | result = f(*args, **kwargs) 22 | if not isinstance(result, Resource): 23 | result = _FakeResource(result) 24 | return result 25 | return inner 26 | 27 | 28 | class APIResource(Resource): 29 | 30 | _registry = None 31 | 32 | def __new__(cls, *args, **kwds): 33 | instance = super().__new__(cls, *args, **kwds) 34 | instance._registry = [] 35 | for name in dir(instance): 36 | attribute = getattr(instance, name) 37 | annotations = getattr(attribute, "__txrestapi__", []) 38 | for annotation in annotations: 39 | method, regex = annotation 40 | instance.register(method, regex, attribute) 41 | return instance 42 | 43 | def __init__(self, *args, **kwargs): 44 | Resource.__init__(self, *args, **kwargs) 45 | if PY2: 46 | self._registry = [] 47 | 48 | def _get_callback(self, request): 49 | filterf = lambda t:t[0] in (request.method, b('ALL')) 50 | path_to_check = getattr(request, '_remaining_path', request.path) 51 | for m, r, cb in filter(filterf, self._registry): 52 | result = r.search(path_to_check) 53 | if result: 54 | request._remaining_path = path_to_check[result.span()[1]:] 55 | return cb, result.groupdict() 56 | return None, None 57 | 58 | def register(self, method, regex, callback): 59 | self._registry.append((method, re.compile(regex.decode()), callback)) 60 | 61 | def unregister(self, method=None, regex=None, callback=None): 62 | if regex is not None: regex = re.compile(regex.decode()) 63 | for m, r, cb in self._registry[:]: 64 | if not method or (method and m==method): 65 | if not regex or (regex and r==regex): 66 | if not callback or (callback and cb==callback): 67 | self._registry.remove((m, r, cb)) 68 | 69 | def getChild(self, name, request): 70 | r = self.children.get(name, None) 71 | if r is None: 72 | # Go into the thing 73 | callback, args = self._get_callback(request) 74 | if callback is None: 75 | return NoResource() 76 | else: 77 | return maybeResource(callback)(request, **args) 78 | else: 79 | return r 80 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Introduction 3 | ============ 4 | 5 | ``txrestapi`` makes it easier to create Twisted REST API services. Normally, one 6 | would create ``Resource`` subclasses defining each segment of a path; this is 7 | cubersome to implement and results in output that isn't very readable. 8 | ``txrestapi`` provides an ``APIResource`` class allowing complex mapping of path to 9 | callback (a la Django) with a readable decorator. 10 | 11 | =============================== 12 | Basic URL callback registration 13 | =============================== 14 | 15 | First, let's create a bare API service:: 16 | 17 | >>> from txrestapi.resource import APIResource 18 | >>> api = APIResource() 19 | 20 | and a web server to serve it:: 21 | 22 | >>> from twisted.web.server import Site 23 | >>> from twisted.internet import reactor 24 | >>> site = Site(api, timeout=None) 25 | 26 | and a function to make it easy for us to make requests (only for doctest 27 | purposes; normally you would of course use ``reactor.listenTCP(8080, site)``):: 28 | 29 | >>> from twisted.web.server import Request 30 | >>> class FakeChannel(object): 31 | ... transport = None 32 | >>> def makeRequest(method, path): 33 | ... req = Request(FakeChannel(), None) 34 | ... req.prepath = req.postpath = None 35 | ... req.method = method; req.path = path 36 | ... resource = site.getChildWithDefault(path, req) 37 | ... return resource.render(req) 38 | 39 | We can now register callbacks for paths we care about. We can provide different 40 | callbacks for different methods; they must accept ``request`` as the first 41 | argument:: 42 | 43 | >>> def get_callback(request): return 'GET callback' 44 | >>> api.register('GET', '^/path/to/method', get_callback) 45 | >>> def post_callback(request): return 'POST callback' 46 | >>> api.register('POST', '^/path/to/method', post_callback) 47 | 48 | Then, when we make a call, the request is routed to the proper callback:: 49 | 50 | >>> print makeRequest('GET', '/path/to/method') 51 | GET callback 52 | >>> print makeRequest('POST', '/path/to/method') 53 | POST callback 54 | 55 | We can register multiple callbacks for different requests; the first one that 56 | matches wins:: 57 | 58 | >>> def default_callback(request): 59 | ... return 'Default callback' 60 | >>> api.register('GET', '^/.*$', default_callback) # Matches everything 61 | >>> print makeRequest('GET', '/path/to/method') 62 | GET callback 63 | >>> print makeRequest('GET', '/path/to/different/method') 64 | Default callback 65 | 66 | Our default callback, however, will only match GET requests. For a true default 67 | callback, we can either register callbacks for each method individually, or we 68 | can use ALL:: 69 | 70 | >>> api.register('ALL', '^/.*$', default_callback) 71 | >>> print makeRequest('PUT', '/path/to/method') 72 | Default callback 73 | >>> print makeRequest('DELETE', '/path/to/method') 74 | Default callback 75 | >>> print makeRequest('GET', '/path/to/method') 76 | GET callback 77 | 78 | Let's unregister all references to the default callback so it doesn't interfere 79 | with later tests (default callbacks should, of course, always be registered 80 | last, so they don't get called before other callbacks):: 81 | 82 | >>> api.unregister(callback=default_callback) 83 | 84 | ============= 85 | URL Arguments 86 | ============= 87 | 88 | Since callbacks accept ``request``, they have access to POST data or query 89 | arguments, but we can also pull arguments out of the URL by using named groups 90 | in the regular expression (similar to Django). These will be passed into the 91 | callback as keyword arguments:: 92 | 93 | >>> def get_info(request, id): 94 | ... return 'Information for id %s' % id 95 | >>> api.register('GET', '/(?P[^/]+)/info$', get_info) 96 | >>> print makeRequest('GET', '/someid/info') 97 | Information for id someid 98 | 99 | Bear in mind all arguments will come in as strings, so code should be 100 | accordingly defensive. 101 | 102 | ================ 103 | Decorator syntax 104 | ================ 105 | 106 | Registration via the ``register()`` method is somewhat awkward, so decorators 107 | are provided making it much more straightforward. :: 108 | 109 | >>> from txrestapi.methods import GET, POST, PUT, ALL 110 | >>> class MyResource(APIResource): 111 | ... 112 | ... @GET('^/(?P[^/]+)/info') 113 | ... def get_info(self, request, id): 114 | ... return 'Info for id %s' % id 115 | ... 116 | ... @PUT('^/(?P[^/]+)/update') 117 | ... @POST('^/(?P[^/]+)/update') 118 | ... def set_info(self, request, id): 119 | ... return "Setting info for id %s" % id 120 | ... 121 | ... @ALL('^/') 122 | ... def default_view(self, request): 123 | ... return "I match any URL" 124 | 125 | Again, registrations occur top to bottom, so methods should be written from 126 | most specific to least. Also notice that one can use the decorator syntax as 127 | one would expect to register a method as the target for two URLs :: 128 | 129 | >>> site = Site(MyResource(), timeout=None) 130 | >>> print makeRequest('GET', '/anid/info') 131 | Info for id anid 132 | >>> print makeRequest('PUT', '/anid/update') 133 | Setting info for id anid 134 | >>> print makeRequest('POST', '/anid/update') 135 | Setting info for id anid 136 | >>> print makeRequest('DELETE', '/anid/delete') 137 | I match any URL 138 | 139 | ====================== 140 | Callback return values 141 | ====================== 142 | 143 | You can return Resource objects from a callback if you wish, allowing you to 144 | have APIs that send you to other kinds of resources, or even other APIs. 145 | Normally, however, you'll most likely want to return strings, which will be 146 | wrapped in a Resource object for convenience. 147 | -------------------------------------------------------------------------------- /txrestapi/json_resource.py: -------------------------------------------------------------------------------- 1 | import re 2 | import json 3 | import time 4 | import six 5 | 6 | from six import PY2 7 | 8 | from functools import wraps 9 | 10 | from twisted.web.resource import Resource 11 | from twisted.web.server import NOT_DONE_YET 12 | from twisted.internet.defer import Deferred 13 | from twisted.python import log as twlog 14 | 15 | 16 | def _to_json(output_object): 17 | return (json.dumps( 18 | output_object, 19 | indent=2, 20 | separators=(',', ': '), 21 | sort_keys=True, 22 | ) + '\n').encode() 23 | 24 | 25 | class _JsonResource(Resource): 26 | _result = '' 27 | isLeaf = True 28 | 29 | def __init__(self, result, executed): 30 | Resource.__init__(self) 31 | self._result = result 32 | self._executed = executed 33 | 34 | def _setHeaders(self, request): 35 | """ 36 | Those headers will allow you to call API methods from web browsers, they require CORS: 37 | https://en.wikipedia.org/wiki/Cross-origin_resource_sharing 38 | """ 39 | request.responseHeaders.addRawHeader(b'content-type', b'application/json') 40 | request.responseHeaders.addRawHeader(b'Access-Control-Allow-Origin', b'*') 41 | request.responseHeaders.addRawHeader(b'Access-Control-Allow-Methods', b'GET, POST, PUT, DELETE') 42 | request.responseHeaders.addRawHeader(b'Access-Control-Allow-Headers', b'x-prototype-version,x-requested-with') 43 | request.responseHeaders.addRawHeader(b'Access-Control-Max-Age', 2520) 44 | return request 45 | 46 | def render(self, request): 47 | self._setHeaders(request) 48 | # this will just add one extra field to the response to populate how fast that API call was processed 49 | self._result['execution'] = '%3.6f' % (time.time() - self._executed) 50 | return _to_json(self._result) 51 | 52 | 53 | class _DelayedJsonResource(_JsonResource): 54 | """ 55 | If your API method returned `Deferred` object instead of final result 56 | we can wait for the result and then return it in API response. 57 | """ 58 | 59 | def _cb(self, result, request): 60 | self._setHeaders(request) 61 | result['execution'] = '%3.6f' % (time.time() - self._executed) 62 | raw = _to_json(result) 63 | if request.channel: 64 | request.write(raw) 65 | request.finish() 66 | else: 67 | twlog.err('REST API connection channel already closed') 68 | 69 | def _eb(self, err, request): 70 | self._setHeaders(request) 71 | execution = '%3.6f' % (time.time() - self._executed) 72 | raw = _to_json(dict(status='ERROR', execution=execution, errors=[str(err), ])) 73 | if request.channel: 74 | request.write(raw) 75 | request.finish() 76 | else: 77 | twlog.err('REST API connection channel already closed') 78 | 79 | def render(self, request): 80 | self._result.addCallback(self._cb, request) 81 | self._result.addErrback(self._eb, request) 82 | return NOT_DONE_YET 83 | 84 | 85 | def maybeResource(f): 86 | @wraps(f) 87 | def inner(*args, **kwargs): 88 | _executed = time.time() 89 | try: 90 | result = f(*args, **kwargs) 91 | 92 | except Exception as exc: 93 | return _JsonResource(dict(status='ERROR', errors=[str(exc), ]), _executed) 94 | 95 | if isinstance(result, Deferred): 96 | return _DelayedJsonResource(result, _executed) 97 | 98 | if not isinstance(result, Resource): 99 | result = _JsonResource(result, _executed) 100 | 101 | return result 102 | return inner 103 | 104 | 105 | class JsonAPIResource(Resource): 106 | 107 | _registry = None 108 | 109 | def __new__(cls, *args, **kwds): 110 | instance = super().__new__(cls, *args, **kwds) 111 | instance._registry = [] 112 | for name in dir(instance): 113 | attribute = getattr(instance, name) 114 | annotations = getattr(attribute, "__txrestapi__", []) 115 | for annotation in annotations: 116 | method, regex = annotation 117 | instance.register(method, regex, attribute) 118 | return instance 119 | 120 | def __init__(self, *args, **kwargs): 121 | Resource.__init__(self, *args, **kwargs) 122 | if PY2: 123 | self._registry = [] 124 | 125 | def _get_callback(self, request): 126 | request_method = request.method 127 | path_to_check = getattr(request, '_remaining_path', request.path) 128 | if not isinstance(path_to_check, six.text_type): 129 | path_to_check = path_to_check.decode() 130 | for m, r, cb in self._registry: 131 | if m == request_method or m == b'ALL': 132 | result = r.search(path_to_check) 133 | if result: 134 | request._remaining_path = path_to_check[result.span()[1]:] 135 | return cb, result.groupdict() 136 | return None, None 137 | 138 | def register(self, method, regex, callback): 139 | if not isinstance(regex, six.text_type): 140 | regex = regex.decode() 141 | self._registry.append((method, re.compile(regex), callback)) 142 | 143 | def unregister(self, method=None, regex=None, callback=None): 144 | if regex is not None: 145 | if not isinstance(regex, six.text_type): 146 | regex = regex.decode() 147 | regex = re.compile(regex) 148 | for m, r, cb in self._registry[:]: 149 | if not method or (method and m == method): 150 | if not regex or (regex and r == regex): 151 | if not callback or (callback and cb == callback): 152 | self._registry.remove((m, r, cb)) 153 | 154 | def getChild(self, name, request): 155 | r = self.children.get(name, None) 156 | if r is None: 157 | # Go into the thing 158 | callback, args = self._get_callback(request) 159 | if callback is None: 160 | return _JsonResource(dict(status='ERROR', errors=['path %r not found' % name, ]), time.time()) 161 | else: 162 | return maybeResource(callback)(request, **args) 163 | else: 164 | return r 165 | -------------------------------------------------------------------------------- /txrestapi/tests.py: -------------------------------------------------------------------------------- 1 | import txrestapi 2 | __package__="txrestapi" 3 | import re 4 | import os.path 5 | import doctest 6 | from six import PY2, b, u 7 | from twisted.internet import reactor 8 | from twisted.internet.defer import inlineCallbacks 9 | from twisted.web.resource import Resource, NoResource 10 | from twisted.web.server import Request, Site 11 | from twisted.web.client import getPage 12 | from twisted.trial import unittest 13 | from .resource import APIResource 14 | from .methods import GET, PUT 15 | 16 | class FakeChannel(object): 17 | transport = None 18 | 19 | def getRequest(method, url): 20 | req = Request(FakeChannel(), None) 21 | req.method = method 22 | req.path = url 23 | return req 24 | 25 | class APIResourceTest(unittest.TestCase): 26 | 27 | def test_returns_normal_resources(self): 28 | r = APIResource() 29 | a = Resource() 30 | r.putChild(b('a'), a) 31 | req = Request(FakeChannel(), None) 32 | a_ = r.getChild(b('a'), req) 33 | self.assertEqual(a, a_) 34 | 35 | def test_registry(self): 36 | compiled = re.compile(b('regex')) 37 | r = APIResource() 38 | r.register(b('GET'), b('regex'), None) 39 | self.assertEqual([x[0] for x in r._registry], [b('GET')]) 40 | self.assertEqual(r._registry[0], (b('GET'), compiled, None)) 41 | 42 | def test_method_matching(self): 43 | r = APIResource() 44 | r.register(b('GET'), b('regex'), 1) 45 | r.register(b('PUT'), b('regex'), 2) 46 | r.register(b('GET'), b('another'), 3) 47 | 48 | req = getRequest(b('GET'), b('regex')) 49 | result = r._get_callback(req) 50 | self.assert_(result) 51 | self.assertEqual(result[0], 1) 52 | 53 | req = getRequest(b('PUT'), b('regex')) 54 | result = r._get_callback(req) 55 | self.assert_(result) 56 | self.assertEqual(result[0], 2) 57 | 58 | req = getRequest(b('GET'), b('another')) 59 | result = r._get_callback(req) 60 | self.assert_(result) 61 | self.assertEqual(result[0], 3) 62 | 63 | req = getRequest(b('PUT'), b('another')) 64 | result = r._get_callback(req) 65 | self.assertEqual(result, (None, None)) 66 | 67 | def test_callback(self): 68 | marker = object() 69 | def cb(request): 70 | return marker 71 | r = APIResource() 72 | r.register(b('GET'), b('regex'), cb) 73 | req = getRequest(b('GET'), b('regex')) 74 | result = r.getChild(b('regex'), req) 75 | self.assertEqual(result.render(req), marker) 76 | 77 | def test_longerpath(self): 78 | marker = object() 79 | r = APIResource() 80 | def cb(request): 81 | return marker 82 | r.register(b('GET'), b('/regex/a/b/c'), cb) 83 | req = getRequest(b('GET'), b('/regex/a/b/c')) 84 | result = r.getChild(b('regex'), req) 85 | self.assertEqual(result.render(req), marker) 86 | 87 | def test_args(self): 88 | r = APIResource() 89 | def cb(request, **kwargs): 90 | return kwargs 91 | r.register(b('GET'), b('/(?P[^/]*)/a/(?P[^/]*)/c'), cb) 92 | req = getRequest(b('GET'), b('/regex/a/b/c')) 93 | result = r.getChild(b('regex'), req) 94 | self.assertEqual(sorted(result.render(req).keys()), ['a', 'b']) 95 | 96 | def test_order(self): 97 | r = APIResource() 98 | def cb1(request, **kwargs): 99 | kwargs.update({'cb1':True}) 100 | return kwargs 101 | def cb(request, **kwargs): 102 | return kwargs 103 | # Register two regexes that will match 104 | r.register(b('GET'), b('/(?P[^/]*)/a/(?P[^/]*)/c'), cb1) 105 | r.register(b('GET'), b('/(?P[^/]*)/a/(?P[^/]*)'), cb) 106 | req = getRequest(b('GET'), b('/regex/a/b/c')) 107 | result = r.getChild(b('regex'), req) 108 | # Make sure the first one got it 109 | self.assert_('cb1' in result.render(req)) 110 | 111 | def test_no_resource(self): 112 | r = APIResource() 113 | r.register(b('GET'), b('^/(?P[^/]*)/a/(?P[^/]*)$'), None) 114 | req = getRequest(b('GET'), b('/definitely/not/a/match')) 115 | result = r.getChild(b('regex'), req) 116 | self.assert_(isinstance(result, NoResource)) 117 | 118 | def test_all(self): 119 | r = APIResource() 120 | def get_cb(r): return b('GET') 121 | def put_cb(r): return b('PUT') 122 | def all_cb(r): return b('ALL') 123 | r.register(b('GET'), b('^path'), get_cb) 124 | r.register(b('ALL'), b('^path'), all_cb) 125 | r.register(b('PUT'), b('^path'), put_cb) 126 | # Test that the ALL registration picks it up before the PUT one 127 | for method in (b('GET'), b('PUT'), b('ALL')): 128 | req = getRequest(method, b('path')) 129 | result = r.getChild(b('path'), req) 130 | self.assertEqual(result.render(req), b('ALL') if method==b('PUT') else method) 131 | 132 | 133 | class TestResource(Resource): 134 | isLeaf = True 135 | def render(self, request): 136 | return b('aresource') 137 | 138 | 139 | class TestAPI(APIResource): 140 | 141 | @GET(b('^/(?Ptest[^/]*)/?')) 142 | def _on_test_get(self, request, a): 143 | return b('GET %s') % a 144 | 145 | @PUT(b('^/(?Ptest[^/]*)/?')) 146 | def _on_test_put(self, request, a): 147 | return b('PUT %s') % a 148 | 149 | @GET(b('^/gettest')) 150 | def _on_gettest(self, request): 151 | return TestResource() 152 | 153 | 154 | class DecoratorsTest(unittest.TestCase): 155 | def _listen(self, site): 156 | return reactor.listenTCP(0, site, interface="127.0.0.1") 157 | 158 | def setUp(self): 159 | r = TestAPI() 160 | site = Site(r, timeout=None) 161 | self.port = self._listen(site) 162 | self.portno = self.port.getHost().port 163 | 164 | def tearDown(self): 165 | return self.port.stopListening() 166 | 167 | def getURL(self, path): 168 | return b("http://127.0.0.1:%d/%s" % (self.portno, path)) 169 | 170 | @inlineCallbacks 171 | def test_get(self): 172 | url = self.getURL('test_thing/') 173 | result = yield getPage(url, method=b('GET')) 174 | self.assertEqual(result, b('GET test_thing')) 175 | 176 | @inlineCallbacks 177 | def test_put(self): 178 | url = self.getURL('test_thing/') 179 | result = yield getPage(url, method=b('PUT')) 180 | self.assertEqual(result, b('PUT test_thing')) 181 | 182 | @inlineCallbacks 183 | def test_resource_wrapper(self): 184 | url = self.getURL('gettest') 185 | result = yield getPage(url, method=b('GET')) 186 | self.assertEqual(result, b('aresource')) 187 | 188 | 189 | def test_suite(): 190 | import unittest as ut 191 | suite = unittest.TestSuite() 192 | suite.addTest(ut.makeSuite(DecoratorsTest)) 193 | suite.addTest(ut.makeSuite(APIResourceTest)) 194 | if PY2: 195 | suite.addTest(doctest.DocFileSuite(os.path.join('..', 'README.rst'))) 196 | return suite 197 | --------------------------------------------------------------------------------