Commit acc7850f authored by Jacek Furmankiewicz's avatar Jacek Furmankiewicz

preparing for 0.0.12

parent 840b2c9f
Twisted REST micro-framework
================================
Based on *Flask* API, with plans integrated multiprocessing support for full usage of all CPUs.
Based on *Flask* API, with plans for integrated multiprocessing support for full usage of all CPUs.
Provides a more Flask/Sinatra-style API on top of the core *twisted.web* APIs.
Geared towards creating REST-oriented server platforms.
......@@ -12,10 +12,10 @@ Single REST module example
The simplest possible REST application:
from corepost.web import CorePost, route
from corepost.web import route, RESTResource
from corepost.enums import Http
class RestApp(CorePost):
class RestApp():
@route("/",Http.GET)
def root(self,request,**kwargs):
......@@ -30,66 +30,157 @@ The simplest possible REST application:
return "%s" % numericid
if __name__ == '__main__':
app = RestApp()
app = RESTResource((RestApp,))
app.run()
Multi-module REST application
--------------------------------
The key CorePost object is just an extension of the regular twisted.web Resource object.
Therefore, it can easily be used to assemble a multi-module REST applications with
different CorePost resources serving from different context paths:
Once can assemble a multi-module REST applications with
different REST services responding from different context paths.
Notice the class *path* attribute which provides a common URL prefix for all REST operations
on a particular service:
from corepost.web import CorePost, route
from corepost.enums import Http
from twisted.web.resource import Resource
from twisted.internet import reactor
from twisted.web.server import Site
from corepost import Response, NotFoundException, AlreadyExistsException
from corepost.web import RESTResource, route, Http
class HomeApp(CorePost):
class DB():
"""Fake in-memory DB for testing"""
customers = {}
@route("/")
def home_root(self,request,**kwargs):
return "HOME %s" % kwargs
@classmethod
def getAllCustomers(cls):
return DB.customers.values()
class Module1(CorePost):
@classmethod
def getCustomer(cls,customerId):
if customerId in DB.customers:
return DB.customers[customerId]
else:
raise NotFoundException("Customer",customerId)
@route("/",Http.GET)
def module1_get(self,request,**kwargs):
return request.path
@classmethod
def saveCustomer(cls,customer):
if customer.customerId in DB.customers:
raise AlreadyExistsException("Customer",customer.customerId)
else:
DB.customers[customer.customerId] = customer
@classmethod
def deleteCustomer(cls,customerId):
if customerId in DB.customers:
del(DB.customers[customerId])
else:
raise NotFoundException("Customer",customerId)
@classmethod
def deleteAllCustomers(cls):
DB.customers.clear()
@classmethod
def getCustomerAddress(cls,customerId,addressId):
c = DB.getCustomer(customerId)
if addressId in c.addresses:
return c.addresses[addressId]
else:
raise NotFoundException("Customer Address",addressId)
class Customer:
"""Represents customer entity"""
def __init__(self,customerId,firstName,lastName):
(self.customerId,self.firstName,self.lastName) = (customerId,firstName,lastName)
self.addresses = {}
class CustomerAddress:
"""Represents customer address entity"""
def __init__(self,streetNumber,streetName,stateCode,countryCode):
(self.streetNumber,self.streetName,self.stateCode,self.countryCode) = (streetNumber,streetName,stateCode,countryCode)
class CustomerRESTService():
path = "/customer"
@route("/")
def getAll(self,request):
return DB.getAllCustomers()
@route("/sub",Http.GET)
def module1e_sub(self,request,**kwargs):
return request.path
@route("/<customerId>")
def get(self,request,customerId):
return DB.getCustomer(customerId)
@route("/",Http.POST)
def post(self,request,customerId,firstName,lastName):
customer = Customer(customerId, firstName, lastName)
DB.saveCustomer(customer)
return Response(201)
@route("/<customerId>",Http.PUT)
def put(self,request,customerId,firstName,lastName):
c = DB.getCustomer(customerId)
(c.firstName,c.lastName) = (firstName,lastName)
return Response(200)
class Module2(CorePost):
@route("/<customerId>",Http.DELETE)
def delete(self,request,customerId):
DB.deleteCustomer(customerId)
return Response(200)
@route("/",Http.GET)
def module2_get(self,request,**kwargs):
return request.path
@route("/",Http.DELETE)
def deleteAll(self,request):
DB.deleteAllCustomers()
return Response(200)
class CustomerAddressRESTService():
path = "/customer/<customerId>/address"
@route("/")
def getAll(self,request,customerId):
return DB.getCustomer(customerId).addresses
@route("/sub",Http.GET)
def module2_sub(self,request,**kwargs):
return request.path
@route("/<addressId>")
def get(self,request,customerId,addressId):
return DB.getCustomerAddress(customerId, addressId)
@route("/",Http.POST)
def post(self,request,customerId,addressId,streetNumber,streetName,stateCode,countryCode):
c = DB.getCustomer(customerId)
address = CustomerAddress(streetNumber,streetName,stateCode,countryCode)
c.addresses[addressId] = address
return Response(201)
@route("/<addressId>",Http.PUT)
def put(self,request,customerId,addressId,streetNumber,streetName,stateCode,countryCode):
address = DB.getCustomerAddress(customerId, addressId)
(address.streetNumber,address.streetName,address.stateCode,address.countryCode) = (streetNumber,streetName,stateCode,countryCode)
return Response(200)
def run_app_multi():
app = Resource()
app.putChild('', HomeApp())
app.putChild('module1',Module1())
app.putChild('module2',Module2())
@route("/<addressId>",Http.DELETE)
def delete(self,request,customerId,addressId):
DB.getCustomerAddress(customerId, addressId) #validate address exists
del(DB.getCustomer(customerId).addresses[addressId])
return Response(200)
@route("/",Http.DELETE)
def deleteAll(self,request,customerId):
c = DB.getCustomer(customerId)
c.addresses = {}
return Response(200)
factory = Site(app)
reactor.listenTCP(8081, factory) #@UndefinedVariable
reactor.run() #@UndefinedVariable
def run_rest_app():
app = RESTResource((CustomerRESTService(),CustomerAddressRESTService()))
app.run(8080)
if __name__ == "__main__":
run_rest_app()
The example above creates 3 modules ("/","module1","/module2") and exposes the following URLs:
The example above creates 2 REST services and exposes the following resources:
http://127.0.0.1:8080
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/sub
http://127.0.0.1:8080/customer
http://127.0.0.1:8080/customer/<customerId>
http://127.0.0.1:8080/customer/<customerId>/address
http://127.0.0.1:8080/customer/<customerId>/address/>addressId>
Path argument extraction
------------------------
......@@ -126,7 +217,7 @@ Argument validation
CorePost integrates the popular 'formencode' package to implement form and query argument validation.
Validators can be specified using a *formencode* Schema object, or via custom field-specific validators, e.g.:
from corepost.web import CorePost, validate, route
from corepost.web import validate, route
from corepost.enums import Http
from formencode import Schema, validators
......@@ -134,7 +225,7 @@ Validators can be specified using a *formencode* Schema object, or via custom fi
allow_extra_fields = True
childId = validators.Regex(regex="^value1|value2$")
class MyApp(CorePost):
class MyApp():
@route("/validate/<int:rootId>/schema",Http.POST)
@validate(schema=TestSchema())
......@@ -283,17 +374,17 @@ A filter class can implement either of them or both (for a wrap around filter),
response.headers["X-Wrap-Output"] = "Output"
In order to activate the filters on a CorePost resource instance, you need to pass a list of them in the constructor as the *filters* parameter, e.g.:
In order to activate the filters on a RESTResource instance, you need to pass a list of them in the constructor as the *filters* parameter, e.g.:
class FilterApp(CorePost):
class FilterApp():
@route("/",Http.GET)
def root(self,request,**kwargs):
return request.received_headers
def run_filter_app():
app = FilterApp(filters=(Change404to503Filter(),AddCustomHeaderFilter(),WrapAroundFilter(),))
app = RESTResource(services=((FilterApp(),),filters=(Change404to503Filter(),AddCustomHeaderFilter(),WrapAroundFilter(),))
app.run(8083)
......@@ -313,4 +404,4 @@ Can be run using:
Plans
-----
* integrate multi core support
\ No newline at end of file
* integrate multi core support
......@@ -6,7 +6,7 @@ Responsible for converting return values into cleanly serializable dict/tuples/l
for JSON/XML/YAML output
'''
import inspect, collections
import collections
from jinja2 import Template
from UserDict import DictMixin
......@@ -19,7 +19,7 @@ def convertForSerialization(obj):
return traverseDict(obj)
elif isClassInstance(obj):
return convertClassToDict(obj)
elif isinstance(obj,collections.Iterable):
elif isinstance(obj,collections.Iterable) and not isinstance(obj,str):
# iterable
values = []
for val in obj:
......@@ -32,34 +32,31 @@ def convertForSerialization(obj):
def convertClassToDict(clazz):
"""Converts a class to a dictionary"""
properties = {}
for prop,val in clazz.__dict__.iteritems():
#omit private fields
if not prop.startswith("_"):
properties[prop] = val
return traverseDict(properties)
def traverseDict(dictObject):
"""Traverses a dict recursively to convertForSerialization any nested classes"""
newDict = {}
for prop,val in dictObject.iteritems():
if inspect.isclass(val):
# call itself recursively
val = convertClassToDict(val)
newDict[prop] = val
newDict[prop] = convertForSerialization(val)
return newDict
def generateXml(object):
def generateXml(obj):
"""Generates basic XML from an object that has already been converted for serialization"""
if isinstance(object,dict):
return str(xmlTemplate.render(item=object.keys()))
elif isinstance(object,collections.Iterable):
return str(xmlListTemplate.render(items=object))
return str(xmlTemplate.render(item=obj.keys()))
elif isinstance(obj,collections.Iterable):
return str(xmlListTemplate.render(items=obj))
else:
raise RuntimeError("Unable to convert to XML: %s" % object)
raise RuntimeError("Unable to convert to XML: %s" % obj)
def isClassInstance(object):
"""Checks if a given object is a class instance"""
return getattr(object, "__class__",None) != None and not isinstance(object,dict) and not isinstance(object,tuple) and not isinstance(object,list)
\ No newline at end of file
def isClassInstance(obj):
"""Checks if a given obj is a class instance"""
return getattr(obj, "__class__",None) != None and not isinstance(obj,dict) and not isinstance(obj,tuple) and not isinstance(obj,list) and not isinstance(obj,str)
......@@ -97,6 +97,7 @@ Feature: REST App
# add 1
When as user 'None:None' I POST 'http://127.0.0.1:8085/customer/d1/address' with 'addressId=HOME&streetNumber=100&streetName=MyStreet&stateCode=CA&countryCode=US'
Then I expect HTTP code 201
# get just the address
When as user 'None:None' I GET 'http://127.0.0.1:8085/customer/d1/address/HOME'
Then I expect HTTP code 200
And I expect JSON content
......@@ -108,4 +109,42 @@ Feature: REST App
"streetNumber": "100"
}
"""
# get the customer with the address
When as user 'None:None' I GET 'http://127.0.0.1:8085/customer/d1'
Then I expect HTTP code 200
And I expect JSON content
"""
{
"addresses": {
"HOME": {
"countryCode": "US",
"stateCode": "CA",
"streetName": "MyStreet",
"streetNumber": "100"
}
},
"customerId": "d1",
"firstName": "John",
"lastName": "Doe1"
}
"""
# update address
When as user 'None:None' I PUT 'http://127.0.0.1:8085/customer/d1/address/HOME' with 'streetNumber=1002&streetName=MyStreet2&stateCode=CA&countryCode=US'
Then I expect HTTP code 200
When as user 'None:None' I GET 'http://127.0.0.1:8085/customer/d1/address/HOME'
Then I expect HTTP code 200
And I expect JSON content
"""
{
"countryCode": "US",
"stateCode": "CA",
"streetName": "MyStreet2",
"streetNumber": "1002"
}
"""
# delete address
When as user 'None:None' I DELETE 'http://127.0.0.1:8085/customer/d1/address/HOME'
Then I expect HTTP code 200
When as user 'None:None' I GET 'http://127.0.0.1:8085/customer/d1/address/HOME'
Then I expect HTTP code 404
\ No newline at end of file
......@@ -10,26 +10,82 @@ The simplest possible twisted.web CorePost REST application:
::
from corepost.web import CorePost, route
from corepost.enums import Http
class CustomerRESTService():
path = "/customer"
class RestApp(CorePost):
@route("/")
def getAll(self,request):
return DB.getAllCustomers()
@route("/<customerId>")
def get(self,request,customerId):
return DB.getCustomer(customerId)
@route("/",Http.POST)
def post(self,request,customerId,firstName,lastName):
customer = Customer(customerId, firstName, lastName)
DB.saveCustomer(customer)
return Response(201)
@route("/<customerId>",Http.PUT)
def put(self,request,customerId,firstName,lastName):
c = DB.getCustomer(customerId)
(c.firstName,c.lastName) = (firstName,lastName)
return Response(200)
@route("/<customerId>",Http.DELETE)
def delete(self,request,customerId):
DB.deleteCustomer(customerId)
return Response(200)
@route("/",Http.DELETE)
def deleteAll(self,request):
DB.deleteAllCustomers()
return Response(200)
class CustomerAddressRESTService():
path = "/customer/<customerId>/address"
@route("/",Http.GET)
def root(self,request,**kwargs):
return request.path
@route("/")
def getAll(self,request,customerId):
return DB.getCustomer(customerId).addresses
@route("/test",Http.GET)
def test(self,request,**kwargs):
return request.path
@route("/<addressId>")
def get(self,request,customerId,addressId):
return DB.getCustomerAddress(customerId, addressId)
@route("/test/<int:numericid>",Http.GET)
def test_get_resources(self,request,numericid,**kwargs):
return "%s" % numericid
@route("/",Http.POST)
def post(self,request,customerId,addressId,streetNumber,streetName,stateCode,countryCode):
c = DB.getCustomer(customerId)
address = CustomerAddress(streetNumber,streetName,stateCode,countryCode)
c.addresses[addressId] = address
return Response(201)
@route("/<addressId>",Http.PUT)
def put(self,request,customerId,addressId,streetNumber,streetName,stateCode,countryCode):
address = DB.getCustomerAddress(customerId, addressId)
(address.streetNumber,address.streetName,address.stateCode,address.countryCode) = (streetNumber,streetName,stateCode,countryCode)
return Response(200)
@route("/<addressId>",Http.DELETE)
def delete(self,request,customerId,addressId):
DB.getCustomerAddress(customerId, addressId) #validate address exists
del(DB.getCustomer(customerId).addresses[addressId])
return Response(200)
@route("/",Http.DELETE)
def deleteAll(self,request,customerId):
c = DB.getCustomer(customerId)
c.addresses = {}
return Response(200)
if __name__ == '__main__':
app = RestApp()
app.run()
def run_rest_app():
app = RESTResource((CustomerRESTService(),CustomerAddressRESTService()))
app.run(8080)
if __name__ == "__main__":
run_rest_app()
Links
`````
......@@ -40,6 +96,11 @@ Links
Changelog
`````````
* 0.0.12:
- backwards incompatible change: added advancer URL routing for nested REST services.
CorePost object is gone, REST services are now just standard classes.
They get wrapped in a RESTResource object (see sample above) when exposed
* 0.0.11:
- added support for request/response filters
* 0.0.10:
......
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