Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Contribute to GitLab
Sign in
Toggle navigation
C
corepost
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Commits
Open sidebar
nexlab
corepost
Commits
27524c6c
Commit
27524c6c
authored
Sep 29, 2011
by
Jacek Furmankiewicz
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
0.0.7: content types
parent
29c6715b
Changes
8
Show whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
238 additions
and
21 deletions
+238
-21
README.md
README.md
+83
-10
enums.py
corepost/enums.py
+1
-0
content_types.feature
corepost/test/feature/content_types.feature
+38
-0
home_resource.py
corepost/test/home_resource.py
+27
-0
steps.py
corepost/test/steps.py
+10
-2
utils.py
corepost/utils.py
+9
-1
web.py
corepost/web.py
+65
-7
setup.py
setup.py
+5
-1
No files found.
README.md
View file @
27524c6c
...
@@ -108,6 +108,18 @@ Example:
...
@@ -108,6 +108,18 @@ Example:
def test(self,request,intarg,floatarg,stringarg,**kwargs):
def test(self,request,intarg,floatarg,stringarg,**kwargs):
pass
pass
@defer.inlineCallbacks support
------------------------------
If you want a deferred async method, just use
*defer.returnValue()*
@route("/",Http.GET)
@defer.inlineCallbacks
def root(self,request,**kwargs):
val1 = yield db.query("SELECT ....")
val2 = yield db.query("SELECT ....")
defer.returnValue(val1 + val2)
Argument validation
Argument validation
-------------------
-------------------
...
@@ -142,17 +154,78 @@ for list of available validators:
...
@@ -142,17 +154,78 @@ for list of available validators:
*
Common
<http://www.formencode.org/en/latest/modules/validators.html#module-formencode.validators>
*
Common
<http://www.formencode.org/en/latest/modules/validators.html#module-formencode.validators>
*
National
<http://www.formencode.org/en/latest/modules/national.html#module-formencode.national>
*
National
<http://www.formencode.org/en/latest/modules/national.html#module-formencode.national>
@defer.inlineCallbacks support
Content types
-------------
-----------------
-------------
If you want a deferred async method, just complete the request yourself, instead of returning a string response
CorePost integrates support for JSON, YAML and XML (partially) based on request content types.
@route("/",Http.GET)
Parsing of incoming content
@defer.inlineCallbacks
^^^^^^^^^^^^^^^^^^^^^^^^^^^
def root(self,request,**kwargs):
val = yield db.query("SELECT ....")
Based on the incoming content type in POST/PUT requests,
request.write(val)
the body will be automatically parsed to JSON, YAML and XML (ElementTree) and attached to the request:
request.finish()
*
request.json
*
request.yaml
*
request.xml
@route("/post/json",(Http.POST,Http.PUT))
def test_json(self,request,**kwargs):
return "%s" % json.dumps(request.json)
@route("/post/xml",(Http.POST,Http.PUT))
def test_xml(self,request,**kwargs):
return "%s" % ElementTree.tostring(request.xml)
@route("/post/yaml",(Http.POST,Http.PUT))
def test_yaml(self,request,**kwargs):
return "%s" % yaml.dump(request.yaml)
Routing requests by incoming content type
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Based on the incoming content type in POST/PUT requests,
the
*same*
URL can be hooked up to different router methods:
@route("/post/by/content",(Http.POST,Http.PUT),MediaType.APPLICATION_JSON)
def test_content_app_json(self,request,**kwargs):
return request.received_headers[HttpHeader.CONTENT_TYPE]
@route("/post/by/content",(Http.POST,Http.PUT),(MediaType.TEXT_XML,MediaType.APPLICATION_XML))
def test_content_xml(self,request,**kwargs):
return request.received_headers[HttpHeader.CONTENT_TYPE]
@route("/post/by/content",(Http.POST,Http.PUT),MediaType.TEXT_YAML)
def test_content_yaml(self,request,**kwargs):
return request.received_headers[HttpHeader.CONTENT_TYPE]
@route("/post/by/content",(Http.POST,Http.PUT))
def test_content_catch_all(self,request,**kwargs):
return MediaType.WILDCARD
Converting Python objects to content type based on what caller can accept
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Instead of returning string responses, the code can just return Python objects.
Depending whether the caller can accept JSON (default) or YAML, the Python objects
will be automatically converted:
@route("/return/by/accept")
def test_return_content_by_accepts(self,request,**kwargs):
val = [{"test1":"Test1"},{"test2":"Test2"}]
return val
Calling this URL with "Accept: application/json" will return:
[{"test1": "Test1"}, {"test2": "Test2"}]
Calling it with "Accept: text/yaml" will return:
- {test1: Test1}
- {test2: Test2}
*Note*
: marshalling to XML will be supported in a future release. There is no default Python library that does this automatically.
HTTP codes
HTTP codes
------------------
------------------
...
...
corepost/enums.py
View file @
27524c6c
...
@@ -14,6 +14,7 @@ class Http:
...
@@ -14,6 +14,7 @@ class Http:
class
HttpHeader
:
class
HttpHeader
:
"""Enumerates common HTTP headers"""
"""Enumerates common HTTP headers"""
CONTENT_TYPE
=
"content-type"
CONTENT_TYPE
=
"content-type"
ACCEPT
=
"accept"
class
MediaType
:
class
MediaType
:
"""Enumerates media types"""
"""Enumerates media types"""
...
...
corepost/test/feature/content_types.feature
View file @
27524c6c
...
@@ -173,3 +173,41 @@ total: 4443.52
...
@@ -173,3 +173,41 @@ total: 4443.52
|
PUT
|
XML
|
<test>1</test>
|
application/xml
|
200
|
|
PUT
|
XML
|
<test>1</test>
|
application/xml
|
200
|
|
PUT
|
YAML
|
test:
2
|
text/yaml
|
200
|
|
PUT
|
YAML
|
test:
2
|
text/yaml
|
200
|
@json
@yaml
@xml
@return_accept
Scenario Outline
:
Return content type based on caller's Accept
When
I prepare HTTP header 'Accept' = '<accept>'
When as user 'None
:
None' I GET 'http
:
//127.0.0.1
:
8080/return/by/accept'
Then
I expect HTTP code
<code>
And
I expect content contains '<content>'
Examples
:
|
content
|
accept
|
code
|
|
[{"test1":
"Test1"},
{"test2":
"Test2"}]
|
application/json
|
200
|
|
Unable
to
convert
String
response
to
XML
automatically
|
application/xml
|
500
|
# not supported yet
|
-
{test1:
Test1}\n-
{test2:
Test2}
|
text/yaml
|
200
|
@json
@yaml
@xml
@return_accept_deferred
Scenario Outline
:
Return content type based on caller's Accept from Deferred methods
When
I prepare HTTP header 'Accept' = '<accept>'
When as user 'None
:
None' I GET 'http
:
//127.0.0.1
:
8080/return/by/accept/deferred'
Then
I expect HTTP code
<code>
And
I expect content contains '<content>'
Examples
:
|
content
|
accept
|
code
|
|
[{"test1":
"Test1"},
{"test2":
"Test2"}]
|
application/json
|
200
|
|
Unable
to
convert
String
response
to
XML
automatically
|
application/xml
|
500
|
# not supported yet
|
-
{test1:
Test1}\n-
{test2:
Test2}
|
text/yaml
|
200
|
@json
@yaml
@xml
@return_accept
Scenario Outline
:
Return class content type based on caller's Accept
When
I prepare HTTP header 'Accept' = '<accept>'
When as user 'None
:
None' I GET 'http
:
//127.0.0.1
:
8080/return/by/accept/class'
Then
I expect HTTP code
<code>
And
I expect content contains '<content>'
Examples
:
|
content
|
accept
|
code
|
|
is
not
JSON
serializable
|
application/json
|
500
|
# not supported yet
|
Unable
to
convert
String
response
to
XML
automatically
|
application/xml
|
500
|
# not supported yet
\ No newline at end of file
corepost/test/home_resource.py
View file @
27524c6c
...
@@ -73,6 +73,33 @@ class HomeApp(CorePost):
...
@@ -73,6 +73,33 @@ class HomeApp(CorePost):
def
test_content_catch_all
(
self
,
request
,
**
kwargs
):
def
test_content_catch_all
(
self
,
request
,
**
kwargs
):
return
MediaType
.
WILDCARD
return
MediaType
.
WILDCARD
##################################################################
# one URL, serving different content types
###################################################################
@
route
(
"/return/by/accept"
)
def
test_return_content_by_accepts
(
self
,
request
,
**
kwargs
):
val
=
[{
"test1"
:
"Test1"
},{
"test2"
:
"Test2"
}]
return
val
@
route
(
"/return/by/accept/deferred"
)
@
defer
.
inlineCallbacks
def
test_return_content_by_accept_deferred
(
self
,
request
,
**
kwargs
):
"""Ensure support for inline callbacks and deferred"""
val
=
yield
[{
"test1"
:
"Test1"
},{
"test2"
:
"Test2"
}]
defer
.
returnValue
(
val
)
@
route
(
"/return/by/accept/class"
)
def
test_return_class_content_by_accepts
(
self
,
request
,
**
kwargs
):
"""Uses Python class instead of dict/list"""
class
Test
:
pass
t1
=
Test
()
t1
.
test1
=
"Test1"
t2
=
Test
()
t2
.
test2
=
"Test2"
val
=
[
t1
,
t2
]
return
val
def
run_app_home
():
def
run_app_home
():
app
=
HomeApp
()
app
=
HomeApp
()
...
...
corepost/test/steps.py
View file @
27524c6c
...
@@ -4,7 +4,7 @@ Common Freshen BDD steps
...
@@ -4,7 +4,7 @@ Common Freshen BDD steps
@author: jacekf
@author: jacekf
'''
'''
from
multiprocessing
import
Process
from
multiprocessing
import
Process
import
httplib2
,
json
,
re
,
time
import
httplib2
,
json
,
re
,
time
,
string
from
freshen
import
Before
,
Given
,
When
,
Then
,
scc
,
glc
,
assert_equals
,
assert_true
#@UnresolvedImport
from
freshen
import
Before
,
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
...
@@ -58,7 +58,7 @@ def when_as_user_i_send_get_delete_to_url(user,password,method,url):
...
@@ -58,7 +58,7 @@ def when_as_user_i_send_get_delete_to_url(user,password,method,url):
h
=
httplib2
.
Http
()
h
=
httplib2
.
Http
()
h
.
follow_redirects
=
False
h
.
follow_redirects
=
False
h
.
add_credentials
(
user
,
password
)
h
.
add_credentials
(
user
,
password
)
scc
.
response
,
scc
.
content
=
h
.
request
(
url
,
method
)
scc
.
response
,
scc
.
content
=
h
.
request
(
url
,
method
,
headers
=
scc
.
http_headers
)
@
When
(
r"^as user '(.+):(.+)' I (POST|PUT) '(.+)' with '(.+)'\s*$"
)
@
When
(
r"^as user '(.+):(.+)' I (POST|PUT) '(.+)' with '(.+)'\s*$"
)
def
when_as_user_i_send_post_put_to_url
(
user
,
password
,
method
,
url
,
params
):
def
when_as_user_i_send_post_put_to_url
(
user
,
password
,
method
,
url
,
params
):
...
@@ -93,6 +93,12 @@ def when_i_define_http_header_with_value(header,value):
...
@@ -93,6 +93,12 @@ def when_i_define_http_header_with_value(header,value):
##################################
##################################
# THEN
# THEN
##################################
##################################
def
transform_content
(
content
):
"""Support embedded newlines"""
if
content
!=
None
:
return
string
.
replace
(
content
,
"
\\
n"
,
"
\n
"
)
else
:
return
None
@
Then
(
r"^I expect HTTP code (\d+)\s*$"
)
@
Then
(
r"^I expect HTTP code (\d+)\s*$"
)
def
expect_http_code
(
code
):
def
expect_http_code
(
code
):
...
@@ -100,10 +106,12 @@ def expect_http_code(code):
...
@@ -100,10 +106,12 @@ def expect_http_code(code):
@
Then
(
r"^I expect content contains '(.+)'\s*$"
)
@
Then
(
r"^I expect content contains '(.+)'\s*$"
)
def
expect_content
(
content
):
def
expect_content
(
content
):
content
=
transform_content
(
content
)
assert_true
(
scc
.
content
.
find
(
content
)
>=
0
,
"Did not find:
\n
%
s
\n
in content:
\n
%
s"
%
(
content
,
scc
.
content
))
assert_true
(
scc
.
content
.
find
(
content
)
>=
0
,
"Did not find:
\n
%
s
\n
in content:
\n
%
s"
%
(
content
,
scc
.
content
))
@
Then
(
r"^I expect content contains\s*$"
)
@
Then
(
r"^I expect content contains\s*$"
)
def
expect_content_multiline
(
content
):
def
expect_content_multiline
(
content
):
content
=
transform_content
(
content
)
assert_true
(
scc
.
content
.
find
(
content
)
>=
0
,
"Did not find:
\n
%
s
\n
in content:
\n
%
s"
%
(
content
,
scc
.
content
))
assert_true
(
scc
.
content
.
find
(
content
)
>=
0
,
"Did not find:
\n
%
s
\n
in content:
\n
%
s"
%
(
content
,
scc
.
content
))
@
Then
(
r"^I expect '([^']*)' header matches '([^']*)'\s*$"
)
@
Then
(
r"^I expect '([^']*)' header matches '([^']*)'\s*$"
)
...
...
corepost/utils.py
View file @
27524c6c
...
@@ -2,6 +2,7 @@
...
@@ -2,6 +2,7 @@
Various CorePost utilities
Various CorePost utilities
'''
'''
from
inspect
import
getargspec
from
inspect
import
getargspec
import
json
def
getMandatoryArgumentNames
(
f
):
def
getMandatoryArgumentNames
(
f
):
'''Returns a tuple of the mandatory arguments required in a function'''
'''Returns a tuple of the mandatory arguments required in a function'''
...
@@ -14,3 +15,10 @@ def getMandatoryArgumentNames(f):
...
@@ -14,3 +15,10 @@ def getMandatoryArgumentNames(f):
def
getRouterKey
(
method
,
url
):
def
getRouterKey
(
method
,
url
):
'''Returns the common key used to represent a function that a request can be routed to'''
'''Returns the common key used to represent a function that a request can be routed to'''
return
"
%
s
%
s"
%
(
method
,
url
)
return
"
%
s
%
s"
%
(
method
,
url
)
def
convertToJson
(
obj
):
"""Converts to JSON, including Python classes that are not JSON serializable by default"""
try
:
return
json
.
dumps
(
obj
)
except
Exception
as
ex
:
raise
RuntimeError
(
str
(
ex
))
corepost/web.py
View file @
27524c6c
...
@@ -5,7 +5,7 @@ Main server classes
...
@@ -5,7 +5,7 @@ Main server classes
'''
'''
from
collections
import
defaultdict
from
collections
import
defaultdict
from
corepost.enums
import
Http
,
HttpHeader
from
corepost.enums
import
Http
,
HttpHeader
from
corepost.utils
import
getMandatoryArgumentNames
from
corepost.utils
import
getMandatoryArgumentNames
,
convertToJson
from
enums
import
MediaType
from
enums
import
MediaType
from
formencode
import
FancyValidator
,
Invalid
from
formencode
import
FancyValidator
,
Invalid
from
twisted.internet
import
reactor
,
defer
from
twisted.internet
import
reactor
,
defer
...
@@ -14,8 +14,7 @@ from twisted.web.resource import Resource
...
@@ -14,8 +14,7 @@ from twisted.web.resource import Resource
from
twisted.web.server
import
Site
,
NOT_DONE_YET
from
twisted.web.server
import
Site
,
NOT_DONE_YET
import
re
,
copy
,
exceptions
,
json
,
yaml
import
re
,
copy
,
exceptions
,
json
,
yaml
from
xml.etree
import
ElementTree
from
xml.etree
import
ElementTree
from
xml.etree.ElementTree
import
Element
class
RequestRouter
:
class
RequestRouter
:
''' Common class for containing info related to routing a request to a function '''
''' Common class for containing info related to routing a request to a function '''
...
@@ -220,12 +219,12 @@ class CorePost(Resource):
...
@@ -220,12 +219,12 @@ class CorePost(Resource):
try
:
try
:
# if POST/PUT, check if we need to automatically parse JSON
# if POST/PUT, check if we need to automatically parse JSON
self
.
__parseRequestData
(
request
)
self
.
__parseRequestData
(
request
)
val
=
urlrouter
.
call
(
self
,
request
,
**
allargs
)
val
=
urlrouter
.
call
(
self
,
request
,
**
allargs
)
#handle Deferreds natively
#handle Deferreds natively
if
isinstance
(
val
,
defer
.
Deferred
):
if
isinstance
(
val
,
defer
.
Deferred
):
# we assume the method will call request.finish()
# add callback to finish the request
val
.
addCallback
(
self
.
__finishDeferred
,
request
)
return
NOT_DONE_YET
return
NOT_DONE_YET
else
:
else
:
#special logic for POST to return 201 (created)
#special logic for POST to return 201 (created)
...
@@ -236,7 +235,8 @@ class CorePost(Resource):
...
@@ -236,7 +235,8 @@ class CorePost(Resource):
else
:
else
:
request
.
setResponseCode
(
201
)
request
.
setResponseCode
(
201
)
return
val
return
self
.
__renderResponse
(
request
,
val
)
except
exceptions
.
TypeError
as
ex
:
except
exceptions
.
TypeError
as
ex
:
return
self
.
__renderError
(
request
,
400
,
"
%
s"
%
ex
)
return
self
.
__renderError
(
request
,
400
,
"
%
s"
%
ex
)
except
Exception
as
ex
:
except
Exception
as
ex
:
...
@@ -248,6 +248,56 @@ class CorePost(Resource):
...
@@ -248,6 +248,56 @@ class CorePost(Resource):
except
Exception
as
ex
:
except
Exception
as
ex
:
return
self
.
__renderError
(
request
,
500
,
"Internal server error:
%
s"
%
ex
)
return
self
.
__renderError
(
request
,
500
,
"Internal server error:
%
s"
%
ex
)
def
__renderResponse
(
self
,
request
,
response
):
"""
Takes care of automatically rendering the response and converting it to appropriate format (text,XML,JSON,YAML)
depending on what the caller can accept
"""
if
isinstance
(
response
,
str
):
return
response
elif
isinstance
(
response
,
Response
):
# TODO
return
"TODO: Response"
else
:
return
self
.
__convertObjectToContentType
(
request
,
response
)
def
__convertObjectToContentType
(
self
,
request
,
obj
):
"""Takes care of converting an object (non-String) response to the appropriate format, based on the what the caller can accept"""
if
HttpHeader
.
ACCEPT
in
request
.
received_headers
:
accept
=
request
.
received_headers
[
HttpHeader
.
ACCEPT
]
if
MediaType
.
APPLICATION_JSON
in
accept
:
request
.
headers
[
HttpHeader
.
CONTENT_TYPE
]
=
MediaType
.
APPLICATION_JSON
return
convertToJson
(
obj
)
elif
MediaType
.
TEXT_YAML
in
accept
:
request
.
headers
[
HttpHeader
.
CONTENT_TYPE
]
=
MediaType
.
TEXT_YAML
return
yaml
.
dump
(
obj
)
elif
MediaType
.
APPLICATION_XML
in
accept
or
MediaType
.
TEXT_XML
in
accept
:
if
isinstance
(
obj
,
Element
):
request
.
headers
[
HttpHeader
.
CONTENT_TYPE
]
=
MediaType
.
APPLICATION_XML
return
ElementTree
.
tostring
(
obj
,
encoding
=
'utf-8'
)
else
:
raise
RuntimeError
(
"Unable to convert String response to XML automatically"
)
else
:
# no idea, let's do JSON
request
.
headers
[
HttpHeader
.
CONTENT_TYPE
]
=
MediaType
.
APPLICATION_JSON
return
convertToJson
(
obj
)
else
:
# called has no accept header, let's default to JSON
request
.
headers
[
HttpHeader
.
CONTENT_TYPE
]
=
MediaType
.
APPLICATION_JSON
return
convertToJson
(
obj
)
def
__finishDeferred
(
self
,
val
,
request
):
"""Finishes any Defered/inlineCallback methods"""
if
not
request
.
finished
:
if
val
!=
None
:
try
:
request
.
write
(
self
.
__renderResponse
(
request
,
val
))
except
Exception
as
ex
:
msg
=
"Unexpected server error:
%
s
\n
%
s"
%
(
type
(
ex
),
ex
)
self
.
__renderError
(
request
,
500
,
msg
)
request
.
write
(
msg
)
request
.
finish
()
def
__renderError
(
self
,
request
,
code
,
message
):
def
__renderError
(
self
,
request
,
code
,
message
):
"""Common method for rendering errors"""
"""Common method for rendering errors"""
request
.
setResponseCode
(
code
)
request
.
setResponseCode
(
code
)
...
@@ -280,6 +330,14 @@ class CorePost(Resource):
...
@@ -280,6 +330,14 @@ class CorePost(Resource):
reactor
.
listenTCP
(
port
,
factory
)
#@UndefinedVariable
reactor
.
listenTCP
(
port
,
factory
)
#@UndefinedVariable
reactor
.
run
()
#@UndefinedVariable
reactor
.
run
()
#@UndefinedVariable
class
Response
:
"""
Custom response object, can be returned instead of raw string response
"""
def
__init__
(
self
,
code
=
200
,
entity
=
None
,
headers
=
{}):
self
.
code
=
200
self
.
entity
=
entity
self
.
headers
=
headers
##################################################################################################
##################################################################################################
#
#
...
...
setup.py
View file @
27524c6c
...
@@ -41,6 +41,10 @@ Links
...
@@ -41,6 +41,10 @@ Links
Changelog
Changelog
`````````
`````````
* 0.0.7 - automatic parsing of incoming content (JSON, YAML, XML)
- routing by incoming content type
- automatic response conversion based on caller's Accept header (JSON/YAML)
- support for defer.returnValue() in @inlineCallbacks route methods
* 0.0.6 - redesigned API around classes and methods, rather than functions and global objects (after feedback from Twisted devs)
* 0.0.6 - redesigned API around classes and methods, rather than functions and global objects (after feedback from Twisted devs)
* 0.0.5 - added FormEncode validation for arguments
* 0.0.5 - added FormEncode validation for arguments
* 0.0.4 - path argument extraction, mandatory argument error checking
* 0.0.4 - path argument extraction, mandatory argument error checking
...
@@ -59,7 +63,7 @@ def read(fname):
...
@@ -59,7 +63,7 @@ def read(fname):
setup
(
setup
(
name
=
"CorePost"
,
name
=
"CorePost"
,
version
=
"0.0.
6
"
,
version
=
"0.0.
7
"
,
author
=
"Jacek Furmankiewicz"
,
author
=
"Jacek Furmankiewicz"
,
author_email
=
"jacekeadE99@gmail.com"
,
author_email
=
"jacekeadE99@gmail.com"
,
description
=
(
"A Twisted Web REST micro-framework"
),
description
=
(
"A Twisted Web REST micro-framework"
),
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment