Commit 021581bb authored by Jacek Furmankiewicz's avatar Jacek Furmankiewicz

multi resource support + BDD tests + docs

parent a08b5e25
...@@ -4,11 +4,13 @@ Twisted REST micro-framework ...@@ -4,11 +4,13 @@ Twisted REST micro-framework
Based on *Flask* API, with integrated multiprocessing support for full usage of all CPUs. Based on *Flask* API, with integrated multiprocessing support for full usage of all CPUs.
Provides a more Flask/Sinatra-style API on top of the core *twisted.web* APIs. Provides a more Flask/Sinatra-style API on top of the core *twisted.web* APIs.
Geared towards creating REST-oriented server platforms (e.g. as a source of data for a Javascript MVC app). Geared towards creating REST-oriented server platforms.
Tested exclusively on PyPy for maximum performance. Tested on PyPy (recommended) and Python 2.7 for maximum performance.
Example Single REST module example
------- --------------------------
The simplest possible REST application:
from corepost.web import CorePost from corepost.web import CorePost
from corepost.enums import Http from corepost.enums import Http
...@@ -28,8 +30,74 @@ Example ...@@ -28,8 +30,74 @@ Example
return "%s - %s" % (numericid,stringid) return "%s - %s" % (numericid,stringid)
if __name__ == '__main__': if __name__ == '__main__':
# shortcut method to run a CorePost Resource as a single site
app.run() app.run()
Multiple module REST application
--------------------------------
The key CorePost object is just an extension of the regular twisted.web Resource object
and as such can easily be used to assemble a multi-module REST applications with
different CorePost objects serving from different context paths:
from corepost.web import CorePost
from corepost.enums import Http
from twisted.web.resource import Resource
from twisted.internet import reactor
from twisted.web.server import Site
# Home module
home = CorePost()
@home.route("/")
def home_root(request,**kwargs):
return "HOME %s" % kwargs
# First module
module1 = CorePost('module1')
@module1.route("/",Http.GET)
def module1_get(request,**kwargs):
return request.path
@module1.route("/sub",Http.GET)
def module1e_sub(request,**kwargs):
return request.path
# Second module
module2 = CorePost('module2')
@module2.route("/",Http.GET)
def module2_get(request,**kwargs):
return request.path
@module2.route("/sub",Http.GET)
def module2_sub(request,**kwargs):
return request.path
if __name__ == '__main__':
app = Resource()
app.putChild(home.path, home)
app.putChild(module1.path,module1)
app.putChild(module2.path,module2)
factory = Site(app)
reactor.listenTCP(8080, factory)
reactor.run()
The example above creates 3 modules ("/","module1","/module2") and exposes the following URLs:
http://127.0.0.1:8080
http://127.0.0.1:8080/
http://127.0.0.1:8080/module1
http://127.0.0.1:8080/module1/
http://127.0.0.1:8080/module1/sub
http://127.0.0.1:8080/module2
http://127.0.0.1:8080/module2/
http://127.0.0.1:8080/module2/sub
@defer.inlineCallbacks support @defer.inlineCallbacks support
------------------------------ ------------------------------
...@@ -47,6 +115,14 @@ Performance ...@@ -47,6 +115,14 @@ Performance
On par with raw *twisted.web* performance. Minimal overhead for URL routing and function argument extraction. On par with raw *twisted.web* performance. Minimal overhead for URL routing and function argument extraction.
BDD unit tests
--------------
All unit tests for CorePost are in BDD feature format, using Freshen.
Can be run using:
nosetests --with-freshen -v
Plans Plans
----- -----
......
...@@ -52,3 +52,20 @@ Feature: URL routing ...@@ -52,3 +52,20 @@ Feature: URL routing
Then I expect HTTP code 200 Then I expect HTTP code 200
And I expect content contains '{'test': 'value', 'test2': 'value2'}' And I expect content contains '{'test': 'value', 'test2': 'value2'}'
@multi
Scenario Outline: Multiple resources with submodules
Given 'multi_resource' is running
When as user 'None:None' I GET '<url>'
Then I expect HTTP code 200
Examples:
| url |
| http://127.0.0.1:8081 |
| http://127.0.0.1:8081/ |
| http://127.0.0.1:8081/module1 |
| http://127.0.0.1:8081/module1/ |
| http://127.0.0.1:8081/module1/sub |
| http://127.0.0.1:8081/module2 |
| http://127.0.0.1:8081/module2/ |
| http://127.0.0.1:8081/module2/sub |
\ No newline at end of file
'''
A CorePost module1 that can be merged into the main CorePost Resource
'''
from corepost.web import CorePost
from corepost.enums import Http
from twisted.web.resource import Resource
from twisted.internet import reactor
from twisted.web.server import Site
home = CorePost()
@home.route("/")
def home_root(request,**kwargs):
return "HOME %s" % kwargs
module1 = CorePost('module1')
@module1.route("/",Http.GET)
def module1_get(request,**kwargs):
return request.path
@module1.route("/sub",Http.GET)
def module1e_sub(request,**kwargs):
return request.path
module2 = CorePost('module2')
@module2.route("/",Http.GET)
def module2_get(request,**kwargs):
return request.path
@module2.route("/sub",Http.GET)
def module2_sub(request,**kwargs):
return request.path
def run_app_multi():
app = Resource()
app.putChild(home.path, home)
app.putChild(module1.path,module1)
app.putChild(module2.path,module2)
factory = Site(app)
reactor.listenTCP(8081, factory) #@UndefinedVariable
reactor.run() #@UndefinedVariable
...@@ -5,11 +5,12 @@ Common Freshen BDD steps ...@@ -5,11 +5,12 @@ Common Freshen BDD steps
''' '''
from multiprocessing import Process from multiprocessing import Process
import httplib2, json, re, time import httplib2, json, re, time
from freshen import Before, After, Given, When, Then, scc, assert_equals, assert_true #@UnresolvedImport from freshen import Before, After, Given, When, Then, scc, glc, assert_equals, assert_true #@UnresolvedImport
from urllib import urlencode from urllib import urlencode
from corepost.test.home_resource import run_app_home from corepost.test.home_resource import run_app_home
from corepost.test.multi_resource import run_app_multi
apps = {'home_resource' : run_app_home} apps = {'home_resource' : run_app_home,'multi_resource':run_app_multi}
def as_dict(parameters): def as_dict(parameters):
dict_val = {} dict_val = {}
...@@ -25,26 +26,25 @@ def as_dict(parameters): ...@@ -25,26 +26,25 @@ def as_dict(parameters):
@Before @Before
def setup(slc): def setup(slc):
scc.processes = []
scc.http_headers = {} scc.http_headers = {}
@After
def cleanup(slc):
# shut down processes
for process in scc.processes:
process.terminate()
################################## ##################################
# GIVEN # GIVEN
################################## ##################################
@Given(r"^'(.+)' is running\s*$") @Given(r"^'(.+)' is running\s*$")
def given_process_is_running(processname): def given_process_is_running(processname):
if glc.processes == None:
glc.processes = {}
if processname not in glc.processes:
# start a process only once, keep it running
# to make test runs faster
process = Process(target=apps[processname]) process = Process(target=apps[processname])
process.daemon = True process.daemon = True
process.start() process.start()
scc.processes.append(process)
time.sleep(0.25) # let it start up time.sleep(0.25) # let it start up
glc.processes[processname] = process
################################## ##################################
# WHEN # WHEN
......
'''
A CorePost submodule that can be merged into the main CorePost Resource
'''
from corepost.web import CorePost
from corepost.enums import Http
submodule = CorePost()
submodule.isLeaf = True
@submodule.route("/test",Http.GET)
def submodule_get(request,**kwargs):
return request.path
\ No newline at end of file
...@@ -96,7 +96,7 @@ class CorePost(Resource): ...@@ -96,7 +96,7 @@ class CorePost(Resource):
''' '''
isLeaf = True isLeaf = True
def __init__(self): def __init__(self,path=''):
''' '''
Constructor Constructor
''' '''
...@@ -104,6 +104,11 @@ class CorePost(Resource): ...@@ -104,6 +104,11 @@ class CorePost(Resource):
self.__urls = defaultdict(dict) self.__urls = defaultdict(dict)
self.__cachedUrls = defaultdict(dict) self.__cachedUrls = defaultdict(dict)
self.__methods = {} self.__methods = {}
self.__path = path
@property
def path(self):
return self.__path
def __registerFunction(self,f,url,methods,accepts,produces,cache): def __registerFunction(self,f,url,methods,accepts,produces,cache):
if f not in self.__methods.values(): if f not in self.__methods.values():
...@@ -116,7 +121,7 @@ class CorePost(Resource): ...@@ -116,7 +121,7 @@ class CorePost(Resource):
self.__methods[url] = f self.__methods[url] = f
def route(self,url,methods=[],accepts=MediaType.WILDCARD,produces=None,cache=True): def route(self,url,methods=(Http.GET,),accepts=MediaType.WILDCARD,produces=None,cache=True):
"""Main decorator for registering REST functions """ """Main decorator for registering REST functions """
def wrap(f): def wrap(f):
self.__registerFunction(f, url, methods, accepts, produces,cache) self.__registerFunction(f, url, methods, accepts, produces,cache)
......
...@@ -16,7 +16,7 @@ def read(fname): ...@@ -16,7 +16,7 @@ def read(fname):
setup( setup(
name="CorePost", name="CorePost",
version="0.0.2", version="0.0.3",
author="Jacek Furmankiewicz", author="Jacek Furmankiewicz",
author_email="jacek99@gmail.com", author_email="jacek99@gmail.com",
description=("A Twisted Web REST micro-framework"), description=("A Twisted Web REST micro-framework"),
...@@ -37,7 +37,6 @@ setup( ...@@ -37,7 +37,6 @@ setup(
], ],
install_requires=[ install_requires=[
'twisted>=11.0.0', 'twisted>=11.0.0',
'twisted.internet.processes>=1.0b1',
'httplib2>=0.7.1', 'httplib2>=0.7.1',
'freshen>=0.2', 'freshen>=0.2',
], ],
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment