├── .gitignore
├── .idea
├── .name
├── codeStyleSettings.xml
├── compiler.xml
├── dictionaries
│ └── dok.xml
├── encodings.xml
├── inspectionProfiles
│ ├── Project_Default.xml
│ └── profiles_settings.xml
├── modules.xml
├── scopes
│ └── scope_settings.xml
└── vcs.xml
├── .project
├── .pydevproject
├── Corepost.iml
├── LICENSE
├── README.md
├── corepost
├── __init__.py
├── convert.py
├── enums.py
├── ext
│ ├── __init__.py
│ ├── multicore
│ │ ├── __init__.py
│ │ └── zmq.py
│ ├── security.py
│ ├── sql.py
│ └── sql
│ │ └── __init__.py
├── filters.py
├── routing.py
├── test
│ ├── __init__.py
│ ├── arguments.py
│ ├── feature
│ │ ├── arguments.feature
│ │ ├── content_types.feature
│ │ ├── filters.feature
│ │ ├── issues.feature
│ │ ├── rest_app.feature
│ │ ├── url_routing.feature
│ │ ├── validate.feature
│ │ └── zeromq_resource.py
│ ├── filter_resource.py
│ ├── home_resource.py
│ ├── multi_resource.py
│ ├── rest_resource.py
│ ├── sql_resource.py
│ └── steps.py
├── utils.py
└── web.py
├── setup.py
└── test.sh
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | build
3 | dist
4 | *.egg-info
5 | .settings
6 | *plugin.xml
7 | .idea
8 |
--------------------------------------------------------------------------------
/.idea/.name:
--------------------------------------------------------------------------------
1 | corepost
--------------------------------------------------------------------------------
/.idea/codeStyleSettings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/.idea/dictionaries/dok.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/.idea/encodings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/scopes/scope_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.project:
--------------------------------------------------------------------------------
1 |
2 |
3 | corepost
4 |
5 |
6 |
7 |
8 |
9 | org.python.pydev.PyDevBuilder
10 |
11 |
12 |
13 |
14 |
15 | org.python.pydev.pythonNature
16 |
17 |
18 |
--------------------------------------------------------------------------------
/.pydevproject:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | python
6 | python 2.7
7 |
8 | /corepost
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Corepost.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD License
2 | --------------------------------------------------------------------
3 | Copyright (c) 2011 Jacek Furmankiewicz
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted provided that the following conditions
8 | are met:
9 | 1. Redistributions of source code must retain the above copyright
10 | notice, this list of conditions and the following disclaimer.
11 | 2. Redistributions in binary form must reproduce the above copyright
12 | notice, this list of conditions and the following disclaimer in the
13 | documentation and/or other materials provided with the distribution.
14 | 3. The name of the author may not be used to endorse or promote products
15 | derived from this software without specific prior written permission.
16 |
17 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
18 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
19 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
20 | IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
21 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
22 | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Twisted REST micro-framework
2 | ================================
3 |
4 | Inspired by the *Flask* API.
5 | Provides a more Flask/Sinatra-style API on top of the core *twisted.web* APIs.
6 |
7 | See our HTML documentation:
8 | http://jacek99.github.com/corepost/
9 |
10 | or the PDF version:
11 | https://github.com/jacek99/corepost/raw/gh-pages/doc/build/latex/CorePost.pdf
12 |
13 | END OF LIFE NOTICE
14 | ==================
15 |
16 | This project is not maintained any more, as I do not work with Python these days.
17 |
18 | If anyone is interested in forking it and taking it over, feel free to contact me.
19 |
--------------------------------------------------------------------------------
/corepost/__init__.py:
--------------------------------------------------------------------------------
1 | '''
2 | Common classes
3 | '''
4 |
5 | from zope.interface import Interface, Attribute
6 |
7 | #########################################################
8 | #
9 | # INTERFACES
10 | #
11 | #########################################################
12 |
13 | class IRESTResource(Interface):
14 | """An interface for all REST services that can be added within a root CorePost resource"""
15 | services = Attribute("All the REST services contained in this resource")
16 |
17 |
18 | #########################################################
19 | #
20 | # CLASSES
21 | #
22 | #########################################################
23 |
24 | class Response:
25 | """
26 | Custom response object, can be returned instead of raw string response
27 | """
28 | def __init__(self,code=200,entity=None,headers={}):
29 | self.code = code
30 | self.entity=entity if entity != None else ""
31 | self.headers=headers
32 |
33 | def __str__(self):
34 | return str(self.__dict__)
35 |
36 | class RESTException(Exception):
37 | """Standard REST exception that gets converted to the Response it passes in"""
38 | def __init__(self, response):
39 | self.response = response
40 |
41 | class NotFoundException(RESTException):
42 | """Standard 404 exception when REST resource is not found"""
43 | def __init__(self, resourceName, invalidValue):
44 | RESTException.__init__(self,Response(404,"Unable to find %s identified by '%s'" % (resourceName,invalidValue), {"x-corepost-resource":resourceName,"x-corepost-value":invalidValue}))
45 |
46 | class ConflictException(RESTException):
47 | """Standard 409 exception when REST resource is not found. Allows to pass in a custom message with more details"""
48 | def __init__(self, resourceName, invalidValue, message):
49 | RESTException.__init__(self,Response(409,"Conflict for %s identified by '%s': %s" % (resourceName,invalidValue, message), {"x-corepost-resource":resourceName,"x-corepost-value":invalidValue}))
50 |
51 | class AlreadyExistsException(ConflictException):
52 | """Standard 409 exception when REST resource already exists during a POST"""
53 | def __init__(self, resourceName, invalidValue, message = None):
54 | ConflictException.__init__(self, resourceName, invalidValue, "%s already exists" % resourceName)
55 |
56 | class InternalServerException(RESTException):
57 | """Standard 500 error"""
58 | def __init__(self, safeErrorMessage):
59 | RESTException.__init__(self,Response(500,safeErrorMessage))
60 |
--------------------------------------------------------------------------------
/corepost/convert.py:
--------------------------------------------------------------------------------
1 | '''
2 | Created on 2011-10-11
3 | @author: jacekf
4 |
5 | Responsible for converting return values into cleanly serializable dict/tuples/lists
6 | for JSON/XML/YAML output
7 | '''
8 |
9 | import collections
10 | import logging
11 | import json
12 | from UserDict import DictMixin
13 | from twisted.python import log
14 |
15 | primitives = (int, long, float, bool, str,unicode)
16 |
17 | def convertForSerialization(obj):
18 | """Converts anything (clas,tuples,list) to the safe serializable equivalent"""
19 | try:
20 | if type(obj) in primitives:
21 | # no conversion
22 | return obj
23 | elif isinstance(obj, dict) or isinstance(obj,DictMixin):
24 | return traverseDict(obj)
25 | elif isClassInstance(obj):
26 | return convertClassToDict(obj)
27 | elif isinstance(obj,collections.Iterable) and not isinstance(obj,str):
28 | # iterable
29 | values = []
30 | for val in obj:
31 | values.append(convertForSerialization(val))
32 | return values
33 | else:
34 | # return as-is
35 | return obj
36 | except AttributeError as ex:
37 | log.msg(ex,logLevel=logging.WARN)
38 | return obj
39 |
40 | def convertClassToDict(clazz):
41 | """Converts a class to a dictionary"""
42 | properties = {}
43 | for prop,val in clazz.__dict__.iteritems():
44 | #omit private fields
45 | if not prop.startswith("_"):
46 | properties[prop] = val
47 |
48 | return traverseDict(properties)
49 |
50 | def traverseDict(dictObject):
51 | """Traverses a dict recursively to convertForSerialization any nested classes"""
52 | newDict = {}
53 |
54 | for prop,val in dictObject.iteritems():
55 | newDict[prop] = convertForSerialization(val)
56 |
57 | return newDict
58 |
59 |
60 | def convertToJson(obj):
61 | """Converts to JSON, including Python classes that are not JSON serializable by default"""
62 | try:
63 | return json.dumps(obj)
64 | except Exception as ex:
65 | raise RuntimeError(str(ex))
66 |
67 | def generateXml(obj):
68 | """Generates basic XML from an object that has already been converted for serialization"""
69 | if isinstance(obj, dict) or isinstance(obj,DictMixin):
70 | return getXML_dict(obj, "item")
71 | elif isinstance(obj,collections.Iterable):
72 | return "%s
" % getXML(obj, "item")
73 | else:
74 | raise RuntimeError("Unable to convert to XML: %s" % obj)
75 |
76 | def isClassInstance(obj):
77 | """Checks if a given obj is a class instance"""
78 | 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)
79 |
80 | ## {{{ http://code.activestate.com/recipes/440595/ (r2)
81 | def getXML(obj, objname=None):
82 | """getXML(obj, objname=None)
83 | returns an object as XML where Python object names are the tags.
84 |
85 | >>> u={'UserID':10,'Name':'Mark','Group':['Admin','Webmaster']}
86 | >>> getXML(u,'User')
87 | '10MarkAdminWebmaster'
88 | """
89 | if obj == None:
90 | return ""
91 | if not objname:
92 | objname = "item"
93 | adapt={
94 | dict: getXML_dict,
95 | list: getXML_list,
96 | tuple: getXML_list,
97 | }
98 | if adapt.has_key(obj.__class__):
99 | return adapt[obj.__class__](obj, objname)
100 | else:
101 | return "<%(n)s>%(o)s%(n)s>"%{'n':objname,'o':str(obj)}
102 |
103 | def getXML_dict(indict, objname=None):
104 | h = "<%s>"%objname
105 | for k, v in indict.items():
106 | h += getXML(v, k)
107 | h += "%s>"%objname
108 | return h
109 |
110 | def getXML_list(inlist, objname=None):
111 | h = ""
112 | for i in inlist:
113 | h += getXML(i, objname)
114 | return h
115 | ## end of http://code.activestate.com/recipes/440595/ }}}
116 |
117 |
--------------------------------------------------------------------------------
/corepost/enums.py:
--------------------------------------------------------------------------------
1 | '''
2 | Common enums
3 |
4 | @author: jacekf
5 | '''
6 |
7 |
8 | class Http:
9 | """Enumerates HTTP methods"""
10 | GET = "GET"
11 | POST = "POST"
12 | PUT = "PUT"
13 | DELETE = "DELETE"
14 | OPTIONS = "OPTIONS"
15 | HEAD = "HEAD"
16 | PATCH = "PATCH"
17 |
18 |
19 | class HttpHeader:
20 | """Enumerates common HTTP headers"""
21 | CONTENT_TYPE = "content-type"
22 | ACCEPT = "accept"
23 |
24 |
25 | class MediaType:
26 | """Enumerates media types"""
27 | WILDCARD = "*/*"
28 | APPLICATION_XML = "application/xml"
29 | APPLICATION_ATOM_XML = "application/atom+xml"
30 | APPLICATION_XHTML_XML = "application/xhtml+xml"
31 | APPLICATION_SVG_XML = "application/svg+xml"
32 | APPLICATION_JSON = "application/json"
33 | APPLICATION_FORM_URLENCODED = "application/x-www-form-urlencoded"
34 | MULTIPART_FORM_DATA = "multipart/form-data"
35 | APPLICATION_OCTET_STREAM = "application/octet-stream"
36 | TEXT_PLAIN = "text/plain"
37 | TEXT_XML = "text/xml"
38 | TEXT_HTML = "text/html"
39 | TEXT_YAML = "text/yaml"
--------------------------------------------------------------------------------
/corepost/ext/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jacek99/corepost/4d4c0efa0af4a113dcd7f22c14c918dbce65c0c9/corepost/ext/__init__.py
--------------------------------------------------------------------------------
/corepost/ext/multicore/__init__.py:
--------------------------------------------------------------------------------
1 | __author__ = 'jacekf'
2 |
--------------------------------------------------------------------------------
/corepost/ext/multicore/zmq.py:
--------------------------------------------------------------------------------
1 | __author__ = 'jacekf'
2 |
3 | try:
4 | import txZMQ
5 | except ImportError as ex:
6 | print "You must have ZeroMQ and txZMQ installed"
7 | raise ex
8 |
9 | from corepost import Response, IRESTResource
10 | from corepost.enums import Http
11 | from corepost.routing import UrlRouter, RequestRouter
12 | from enums import MediaType
13 | from formencode import FancyValidator, Invalid
14 | from twisted.internet import reactor
15 | from twisted.internet.defer import Deferred
16 | from twisted.web.resource import Resource
17 | from twisted.web.server import Site, NOT_DONE_YET
18 | from zope.interface import implements
19 |
20 | class ZMQResource(Resource):
21 | """
22 | Responsible for intercepting HTTP requests and marshalling them via ZeroMQ to responders in the process pool
23 | """
24 | isLeaf = True
25 | implements(IRESTResource)
26 |
27 | def __init__(self):
28 | '''
29 | Constructor
30 | '''
31 | Resource.__init__(self)
32 |
33 | def render(self, request):
34 | """Posts request to ZeroMQ and waits for response"""
35 | pass
36 |
37 |
38 | class ZMQResponder:
39 | """
40 | Responsible for processing an incoming request via ZeroMQ and responding via a REST API as if it were a direct HTTP request
41 | """
42 | def __init__(self,services=(),schema=None,filters=()):
43 | '''
44 | Constructor
45 | '''
46 | self.services = services
47 | self.__router = RequestRouter(self,schema,filters)
48 |
--------------------------------------------------------------------------------
/corepost/ext/security.py:
--------------------------------------------------------------------------------
1 | '''
2 | Enhancements to core Twisted security
3 | @author: jacekf
4 | '''
5 |
6 | from twisted.cred.checkers import ICredentialsChecker
7 | from zope.interface import implements
8 |
9 | from beaker.cache import CacheManager
10 | from beaker.util import parse_cache_config_options
11 |
12 | class Principal:
13 | '''A security principal with privileges attached to it'''
14 | def __init__(self,userId,privileges=None):
15 | '''
16 | @param userId -- mandatory user ID
17 | @param privileges -- list of privileges assigned to this user
18 | '''
19 | self.__userId = userId
20 | self.__privileges = privileges
21 |
22 | @property
23 | def userId(self):
24 | return self.__userId
25 |
26 | @property
27 | def privileges(self):
28 | return self.__privileges
29 |
30 | class CachedCredentialsChecker:
31 | """A cached credentials checker wrapper. It will forward calls to the actual credentials checker only when the cache expires (or on first call)"""
32 | implements(ICredentialsChecker)
33 |
34 | def __init__(self,credentialInterfaces,credentialsChecker):
35 | self.credentialInterfaces = credentialInterfaces
36 | self.checker = credentialsChecker
37 |
38 | #initialize cache
39 | cacheOptions = {
40 | 'cache.type': 'memory',
41 | }
42 | self.cache = CacheManager(**parse_cache_config_options(cacheOptions))
43 |
44 | def requestAvatarId(self,credentials):
45 | pass
46 |
47 |
48 | ##################################################################################################
49 | #
50 | # DECORATORS
51 | #
52 | ##################################################################################################
53 |
54 | def secured(privileges=None):
55 | '''
56 | Main decorator for securing REST endpoints via roles
57 | '''
58 | pass
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/corepost/ext/sql.py:
--------------------------------------------------------------------------------
1 | '''
2 | Created on 2012-04-17
3 |
4 | @author: jacekf
5 | '''
6 |
7 | class SqlEntityService:
8 | pass
9 |
--------------------------------------------------------------------------------
/corepost/ext/sql/__init__.py:
--------------------------------------------------------------------------------
1 | '''
2 | Multicore module
3 |
4 | @author: jacekf
5 | '''
6 |
7 | try:
8 | import txZMQ
9 | except ImportError as ex:
10 | print "You need to have txZMQ and ZeroMQ installed in order to use multicore support in Corepost"
11 | raise ex
12 |
13 |
--------------------------------------------------------------------------------
/corepost/filters.py:
--------------------------------------------------------------------------------
1 | '''
2 | Various filters & interceptors
3 | @author: jacekf
4 | '''
5 | from zope.interface import Interface
6 |
7 | class IRequestFilter(Interface):
8 | """Request filter interface"""
9 | def filterRequest(self,request):
10 | """Allows to intercept and change an incoming request"""
11 | pass
12 |
13 | class IResponseFilter(Interface):
14 | """Response filter interface"""
15 | def filterResponse(self,request,response):
16 | """Allows to intercept and change an outgoing response"""
17 | pass
18 |
--------------------------------------------------------------------------------
/corepost/routing.py:
--------------------------------------------------------------------------------
1 | '''
2 | Created on 2011-10-03
3 | @author: jacekf
4 |
5 | Common routing classes, regardless of whether used in HTTP or multiprocess context
6 | '''
7 | from collections import defaultdict
8 | from corepost import Response, RESTException
9 | from corepost.enums import Http, HttpHeader
10 | from corepost.utils import getMandatoryArgumentNames, safeDictUpdate
11 | from corepost.convert import convertForSerialization, generateXml, convertToJson
12 | from corepost.filters import IRequestFilter, IResponseFilter
13 |
14 | from enums import MediaType
15 | from twisted.internet import defer
16 | from twisted.web.http import parse_qs
17 | from twisted.python import log
18 | import re, copy, exceptions, yaml,json, logging
19 | from xml.etree import ElementTree
20 | import uuid
21 |
22 |
23 | class UrlRouter:
24 | ''' Common class for containing info related to routing a request to a function '''
25 |
26 | __urlMatcher = re.compile(r"<(int|float|uuid|):?([^/]+)>")
27 | __urlRegexReplace = {"":r"(?P([^/]+))","int":r"(?P\d+)","float":r"(?P\d+.?\d*)","uuid":r"(?P[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})"}
28 | __typeConverters = {"int":int,"float":float,"uuid":uuid.UUID}
29 |
30 | def __init__(self,f,url,methods,accepts,produces,cache):
31 | self.__f = f
32 | self.__url = url
33 | self.__methods = methods if isinstance(methods,tuple) else (methods,)
34 | self.__accepts = accepts if isinstance(accepts,tuple) else (accepts,)
35 | self.__produces = produces
36 | self.__cache = cache
37 | self.__argConverters = {} # dict of arg names -> group index
38 | self.__validators = {}
39 | self.__mandatory = getMandatoryArgumentNames(f)[2:]
40 |
41 | def compileMatcherForFullUrl(self):
42 | """Compiles the regex matches once the URL has been updated to include the full path from the parent class"""
43 | #parse URL into regex used for matching
44 | m = UrlRouter.__urlMatcher.findall(self.url)
45 | self.__matchUrl = "^%s$" % self.url
46 | for match in m:
47 | if len(match[0]) == 0:
48 | # string
49 | self.__argConverters[match[1]] = None
50 | self.__matchUrl = self.__matchUrl.replace("<%s>" % match[1],
51 | UrlRouter.__urlRegexReplace[match[0]].replace("arg",match[1]))
52 | else:
53 | # non string
54 | self.__argConverters[match[1]] = UrlRouter.__typeConverters[match[0]]
55 | self.__matchUrl = self.__matchUrl.replace("<%s:%s>" % match,
56 | UrlRouter.__urlRegexReplace[match[0]].replace("arg",match[1]))
57 |
58 | self.__matcher = re.compile(self.__matchUrl)
59 |
60 |
61 | @property
62 | def cache(self):
63 | '''Indicates if this URL should be cached or not'''
64 | return self.__cache
65 |
66 | @property
67 | def methods(self):
68 | return self.__methods
69 |
70 | @property
71 | def url(self):
72 | return self.__url
73 |
74 | @property
75 | def accepts(self):
76 | return self.__accepts
77 |
78 | def addValidator(self,fieldName,validator):
79 | '''Adds additional field-specific formencode validators'''
80 | self.__validators[fieldName] = validator
81 |
82 | def getArguments(self,url):
83 | '''
84 | Returns None if nothing matched (i.e. URL does not match), empty dict if no args found (i,e, static URL)
85 | or dict with arg/values for dynamic URLs
86 | '''
87 | g = self.__matcher.search(url)
88 | if g != None:
89 | args = g.groupdict()
90 | # convert to expected datatypes
91 | if len(args) > 0:
92 | for name in args.keys():
93 | converter = self.__argConverters[name]
94 | if converter != None:
95 | args[name] = converter(args[name])
96 | return args
97 | else:
98 | return None
99 |
100 | def call(self,instance,request,**kwargs):
101 | '''Forwards call to underlying method'''
102 | for arg in self.__mandatory:
103 | if arg not in kwargs:
104 | raise TypeError("Missing mandatory argument '%s'" % arg)
105 | return self.__f(instance,request,**kwargs)
106 |
107 | def __str__(self):
108 | return "%s %s" % (self.url, self.methods)
109 |
110 | class UrlRouterInstance():
111 | """Combines a UrlRouter with a class instance it should be executed against"""
112 | def __init__(self,clazz,urlRouter):
113 | self.clazz = clazz
114 | self.urlRouter = urlRouter
115 |
116 | def __str__(self):
117 | return self.urlRouter.url
118 |
119 | class CachedUrl:
120 | '''
121 | Used for caching URLs that have been already routed once before. Avoids the overhead
122 | of regex processing on every incoming call for commonly accessed REST URLs
123 | '''
124 | def __init__(self,urlRouterInstance,args):
125 | self.__urlRouterInstance = urlRouterInstance
126 | self.__args = args
127 |
128 | @property
129 | def urlRouterInstance(self):
130 | return self.__urlRouterInstance
131 |
132 | @property
133 | def args(self):
134 | return self.__args
135 |
136 | class RequestRouter:
137 | '''
138 | Class that handles request->method routing functionality to any type of resource
139 | '''
140 |
141 | def __init__(self,restServiceContainer,schema=None,filters=()):
142 | '''
143 | Constructor
144 | '''
145 | self.__urls = {Http.GET: defaultdict(dict),Http.POST: defaultdict(dict),Http.PUT: defaultdict(dict),Http.DELETE: defaultdict(dict),Http.OPTIONS: defaultdict(dict),Http.PATCH: defaultdict(dict),Http.HEAD: defaultdict(dict)}
146 | self.__cachedUrls = {Http.GET: defaultdict(dict),Http.POST: defaultdict(dict),Http.PUT: defaultdict(dict),Http.DELETE: defaultdict(dict),Http.OPTIONS: defaultdict(dict),Http.PATCH: defaultdict(dict),Http.HEAD: defaultdict(dict)}
147 | self.__urlRouterInstances = {}
148 | self.__schema = schema
149 | self.__urlsMehods = {}
150 | self.__registerRouters(restServiceContainer)
151 | self.__urlContainer = restServiceContainer
152 | self.__requestFilters = []
153 | self.__responseFilters = []
154 |
155 | if filters != None:
156 | for webFilter in filters:
157 | valid = False
158 | if IRequestFilter.providedBy(webFilter):
159 | self.__requestFilters.append(webFilter)
160 | valid = True
161 | if IResponseFilter.providedBy(webFilter):
162 | self.__responseFilters.append(webFilter)
163 | valid = True
164 |
165 | if not valid:
166 | raise RuntimeError("filter %s must implement IRequestFilter or IResponseFilter" % webFilter.__class__.__name__)
167 |
168 | @property
169 | def path(self):
170 | return self.__path
171 |
172 | def __registerRouters(self, restServiceContainer):
173 | """Main method responsible for registering routers"""
174 | from types import FunctionType
175 |
176 | for service in restServiceContainer.services:
177 | # check if the service has a root path defined, which is optional
178 | rootPath = service.__class__.path if "path" in service.__class__.__dict__ else ""
179 |
180 | for key in service.__class__.__dict__:
181 | func = service.__class__.__dict__[key]
182 | # handle REST resources directly on the CorePost resource
183 | if type(func) == FunctionType and hasattr(func,'corepostRequestRouter'):
184 | # if specified, add class path to each function's path
185 | rq = func.corepostRequestRouter
186 | #workaround for multiple passes of __registerRouters (for unit tests etc)
187 | if not hasattr(rq, 'urlAdapted'):
188 | rq.url = "%s%s" % (rootPath,rq.url)
189 | # remove first and trailing '/' to standardize URLs
190 | start = 1 if rq.url[0:1] == "/" else 0
191 | end = -1 if rq.url[len(rq.url) -1] == '/' else len(rq.url)
192 | rq.url = rq.url[start:end]
193 | setattr(rq,'urlAdapted',True)
194 |
195 | # now that the full URL is set, compile the matcher for it
196 | rq.compileMatcherForFullUrl()
197 | for method in rq.methods:
198 | for accepts in rq.accepts:
199 | urlRouterInstance = UrlRouterInstance(service,rq)
200 | self.__urls[method][rq.url][accepts] = urlRouterInstance
201 | self.__urlRouterInstances[func] = urlRouterInstance # needed so that we can lookup the urlRouterInstance for a specific function
202 | if self.__urlsMehods.get(rq.url, None) is None:
203 | self.__urlsMehods[rq.url] = []
204 | self.__urlsMehods[rq.url].append(method)
205 |
206 | def getResponse(self,request):
207 | """Finds the appropriate instance and dispatches the request to the registered function. Returns the appropriate Response object"""
208 | # see if already cached
209 | response = None
210 | try:
211 | if len(self.__requestFilters) > 0:
212 | self.__filterRequests(request)
213 |
214 | # standardize URL and remove trailing "/" if necessary
215 | standardized_postpath = request.postpath if (request.postpath[-1] != '' or request.postpath == ['']) else request.postpath[:-1]
216 | path = '/'.join(standardized_postpath)
217 |
218 | contentType = MediaType.WILDCARD if HttpHeader.CONTENT_TYPE not in request.received_headers else request.received_headers[HttpHeader.CONTENT_TYPE]
219 |
220 | urlRouterInstance, pathargs = None, None
221 | # fetch URL arguments <-> function from cache if hit at least once before
222 | if contentType in self.__cachedUrls[request.method][path]:
223 | cachedUrl = self.__cachedUrls[request.method][path][contentType]
224 | urlRouterInstance,pathargs = cachedUrl.urlRouterInstance, cachedUrl.args
225 | else:
226 | # first time this URL is called
227 | instance = None
228 |
229 | # go through all the URLs, pick up the ones matching by content type
230 | # and then validate which ones match by path/argument to a particular UrlRouterInstance
231 | for contentTypeInstances in self.__urls[request.method].values():
232 |
233 | if contentType in contentTypeInstances:
234 | # there is an exact function for this incoming content type
235 | instance = contentTypeInstances[contentType]
236 | elif MediaType.WILDCARD in contentTypeInstances:
237 | # fall back to any wildcard method
238 | instance = contentTypeInstances[MediaType.WILDCARD]
239 |
240 | if instance != None:
241 | # see if the path arguments match up against any function @route definition
242 | args = instance.urlRouter.getArguments(path)
243 | if args != None:
244 |
245 | if instance.urlRouter.cache:
246 | self.__cachedUrls[request.method][path][contentType] = CachedUrl(instance, args)
247 | urlRouterInstance,pathargs = instance,args
248 | break
249 | #actual call
250 | if urlRouterInstance != None and pathargs != None:
251 | allargs = copy.deepcopy(pathargs)
252 |
253 | try:
254 | # if POST/PUT, check if we need to automatically parse JSON, YAML, XML
255 | self.__parseRequestData(request)
256 | # parse request arguments from form or JSON docss
257 | self.__addRequestArguments(request, allargs)
258 | urlRouter = urlRouterInstance.urlRouter
259 | val = urlRouter.call(urlRouterInstance.clazz,request,**allargs)
260 |
261 | #handle Deferreds natively
262 | if isinstance(val,defer.Deferred):
263 | # add callback to finish the request
264 | val.addCallback(self.__finishDeferred,request)
265 | val.addErrback(self.__finishDeferredError,request)
266 | return val
267 | else:
268 | #special logic for POST to return 201 (created)
269 | if request.method == Http.POST:
270 | if hasattr(request, 'code'):
271 | if request.code == 200:
272 | request.setResponseCode(201)
273 | else:
274 | request.setResponseCode(201)
275 |
276 | response = self.__generateResponse(request, val, request.code)
277 |
278 | except exceptions.TypeError as ex:
279 | log.msg(ex,logLevel=logging.WARN)
280 | response = self.__createErrorResponse(request,400,"%s" % ex)
281 |
282 | except RESTException as ex:
283 | """Convert REST exceptions to their responses. Input errors log at a lower level to avoid overloading logs"""
284 | if (ex.response.code in (400,404)):
285 | log.msg(ex,logLevel=logging.WARN)
286 | else:
287 | log.err(ex)
288 | response = ex.response
289 |
290 | except Exception as ex:
291 | log.err(ex)
292 | response = self.__createErrorResponse(request,500,"Unexpected server error: %s\n%s" % (type(ex),ex))
293 |
294 | #if a url is defined, but not the requested method
295 | elif not request.method in self.__urlsMehods.get(path, []) and self.__urlsMehods.get(path, []) != []:
296 |
297 | response = self.__createErrorResponse(request,501, "")
298 | else:
299 | log.msg("URL %s not found" % path,logLevel=logging.WARN)
300 | response = self.__createErrorResponse(request,404,"URL '%s' not found\n" % request.path)
301 |
302 | except Exception as ex:
303 | log.err(ex)
304 | response = self.__createErrorResponse(request,500,"Internal server error: %s" % ex)
305 |
306 | # response handling
307 | if response != None and len(self.__responseFilters) > 0:
308 | self.__filterResponses(request,response)
309 |
310 | return response
311 |
312 | def __generateResponse(self,request,response,code=200):
313 | """
314 | Takes care of automatically rendering the response and converting it to appropriate format (text,XML,JSON,YAML)
315 | depending on what the caller can accept. Returns Response
316 | """
317 | if isinstance(response, str):
318 | return Response(code,response,{HttpHeader.CONTENT_TYPE: MediaType.TEXT_PLAIN})
319 | elif isinstance(response, Response):
320 | return response
321 | else:
322 | (content,contentType) = self.__convertObjectToContentType(request, response)
323 | return Response(code,content,{HttpHeader.CONTENT_TYPE:contentType})
324 |
325 | def __convertObjectToContentType(self,request,obj):
326 | """
327 | Takes care of converting an object (non-String) response to the appropriate format, based on the what the caller can accept.
328 | Returns a tuple of (content,contentType)
329 | """
330 | obj = convertForSerialization(obj)
331 |
332 | if HttpHeader.ACCEPT in request.received_headers:
333 | accept = request.received_headers[HttpHeader.ACCEPT]
334 | if MediaType.APPLICATION_JSON in accept:
335 | return (convertToJson(obj),MediaType.APPLICATION_JSON)
336 | elif MediaType.TEXT_YAML in accept:
337 | return (yaml.dump(obj),MediaType.TEXT_YAML)
338 | elif MediaType.APPLICATION_XML in accept or MediaType.TEXT_XML in accept:
339 | return (generateXml(obj),MediaType.APPLICATION_XML)
340 | else:
341 | # no idea, let's do JSON
342 | return (convertToJson(obj),MediaType.APPLICATION_JSON)
343 | else:
344 | # called has no accept header, let's default to JSON
345 | return (convertToJson(obj),MediaType.APPLICATION_JSON)
346 |
347 | def __finishDeferred(self,val,request):
348 | """Finishes any Defered/inlineCallback methods. Returns Response"""
349 | if isinstance(val,Response):
350 | return val
351 | elif val != None:
352 | try:
353 | return self.__generateResponse(request,val)
354 | except Exception as ex:
355 | msg = "Unexpected server error: %s\n%s" % (type(ex),ex)
356 | return self.__createErrorResponse(request, 500, msg)
357 | else:
358 | return Response(209,None)
359 |
360 | def __finishDeferredError(self,error,request):
361 | """Finishes any Defered/inlineCallback methods that raised an error. Returns Response"""
362 | log.err(error, "Deferred failed")
363 | return self.__createErrorResponse(request, 500,"Internal server error")
364 |
365 | def __createErrorResponse(self,request,code,message):
366 | """Common method for rendering errors"""
367 | return Response(code=code, entity=message, headers={"content-type": MediaType.TEXT_PLAIN})
368 |
369 | def __parseRequestData(self,request):
370 | '''Automatically parses JSON,XML,YAML if present'''
371 | if request.method in (Http.POST,Http.PUT) and HttpHeader.CONTENT_TYPE in request.received_headers.keys():
372 | contentType = request.received_headers["content-type"]
373 | request.data = request.content.read()
374 |
375 | if contentType == MediaType.APPLICATION_JSON:
376 | try:
377 | request.json = json.loads(request.data) if request.data else {}
378 | except Exception as ex:
379 | raise TypeError("Unable to parse JSON body: %s" % ex)
380 | elif contentType in (MediaType.APPLICATION_XML,MediaType.TEXT_XML):
381 | try:
382 | request.xml = ElementTree.XML(request.data)
383 | except Exception as ex:
384 | raise TypeError("Unable to parse XML body: %s" % ex)
385 | elif contentType == MediaType.TEXT_YAML:
386 | try:
387 | request.yaml = yaml.safe_load(request.data)
388 | except Exception as ex:
389 | raise TypeError("Unable to parse YAML body: %s" % ex)
390 |
391 | def __addRequestArguments(self,request,allargs):
392 | """Parses the request form arguments OR JSON document root elements to build the list of arguments to a method"""
393 | # handler for weird Twisted logic where PUT does not get form params
394 | # see: http://twistedmatrix.com/pipermail/twisted-web/2007-March/003338.html
395 | requestargs = request.args
396 |
397 | if request.method == Http.PUT and HttpHeader.CONTENT_TYPE in request.received_headers.keys() \
398 | and request.received_headers[HttpHeader.CONTENT_TYPE] == MediaType.APPLICATION_FORM_URLENCODED:
399 | # request.data is populated in __parseRequestData
400 | requestargs = parse_qs(request.data, 1)
401 |
402 | #merge form args
403 | if len(requestargs.keys()) > 0:
404 | for arg in requestargs.keys():
405 | # maintain first instance of an argument always
406 | safeDictUpdate(allargs,arg,requestargs[arg][0])
407 | elif hasattr(request,'json'):
408 | # if YAML parse root elements instead of form elements
409 | for key in request.json.keys():
410 | safeDictUpdate(allargs, key, request.json[key])
411 | elif hasattr(request,'yaml'):
412 | # if YAML parse root elements instead of form elements
413 | for key in request.yaml.keys():
414 | safeDictUpdate(allargs, key, request.yaml[key])
415 | elif hasattr(request,'xml'):
416 | # if XML, parse attributes first, then root nodes
417 | for key in request.xml.attrib:
418 | safeDictUpdate(allargs, key, request.xml.attrib[key])
419 | for el in request.xml.findall("*"):
420 | safeDictUpdate(allargs, el.tag,el.text)
421 |
422 |
423 | def __filterRequests(self,request):
424 | """Filters incoming requests"""
425 | for webFilter in self.__requestFilters:
426 | webFilter.filterRequest(request)
427 |
428 | def __filterResponses(self,request,response):
429 | """Filters incoming requests"""
430 | for webFilter in self.__responseFilters:
431 | webFilter.filterResponse(request,response)
--------------------------------------------------------------------------------
/corepost/test/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jacek99/corepost/4d4c0efa0af4a113dcd7f22c14c918dbce65c0c9/corepost/test/__init__.py
--------------------------------------------------------------------------------
/corepost/test/arguments.py:
--------------------------------------------------------------------------------
1 | '''
2 | Argument extraction tests
3 | @author: jacekf
4 | '''
5 |
6 | from corepost.web import RESTResource, validate, route
7 | from corepost.enums import Http
8 | from formencode import Schema, validators
9 |
10 | class TestSchema(Schema):
11 | allow_extra_fields = True
12 | childId = validators.Regex(regex="^jacekf|test$")
13 |
14 | class ArgumentApp():
15 |
16 | @route("/int//float//string/",Http.GET)
17 | def test(self,request,intarg,floatarg,stringarg,**kwargs):
18 | args = (intarg,floatarg,stringarg)
19 | return "%s" % map(lambda x: (type(x),x),args)
20 |
21 | @route("/validate//schema",Http.POST)
22 | @validate(schema=TestSchema())
23 | def postValidateSchema(self,request,rootId,childId,**kwargs):
24 | return "%s - %s - %s" % (rootId,childId,kwargs)
25 |
26 | @route("/validate//custom",Http.POST)
27 | @validate(childId=validators.Regex(regex="^jacekf|test$"))
28 | def postValidateCustom(self,request,rootId,childId,**kwargs):
29 | return "%s - %s - %s" % (rootId,childId,kwargs)
30 |
31 | @route("/formOrJson",Http.GET)
32 | def getArgumentsByContentType(self,request,first,last,**kwargs):
33 | return "%s %s" % (str(first),str(last))
34 |
35 | @route("/formOrJson",(Http.POST,Http.PUT))
36 | def postArgumentsByContentType(self,request,first,last,**kwargs):
37 | return "%s %s" % (str(first),str(last))
38 |
39 |
40 | def run_app_arguments():
41 | app = RESTResource((ArgumentApp(),))
42 | app.run(8082)
--------------------------------------------------------------------------------
/corepost/test/feature/arguments.feature:
--------------------------------------------------------------------------------
1 | Using step definitions from: '../steps'
2 |
3 | @arguments
4 | Feature: Arguments
5 | CorePost should be able to correctly extract arguments
6 | from paths, query arguments, form arguments and JSON documents
7 |
8 | @arguments_ok
9 | Scenario Outline: Path argument extraction
10 | Given 'arguments' is running
11 | When as user 'None:None' I GET 'http://127.0.0.1:8082'
12 | Then I expect HTTP code
13 | And I expect content contains ''
14 |
15 | Examples:
16 | | url | code | content |
17 | | /int/1/float/1.1/string/TEST | 200 | [(, 1), (, 1.1), (, 'TEST')] |
18 | | /int/1/float/1/string/TEST | 200 | [(, 1), (, 1.0), (, 'TEST')] |
19 | | /int/1/float/1/string/23 | 200 | [(, 1), (, 1.0), (, '23')] |
20 |
21 | @arguments_error
22 | Scenario Outline: Path argument extraction - error handling
23 | Given 'arguments' is running
24 | When as user 'None:None' I GET 'http://127.0.0.1:8082'
25 | Then I expect HTTP code
26 | And I expect content contains ''
27 |
28 | Examples:
29 | | url | code | content |
30 | | /int/WRONG/float/1.1/string/TEST | 404 | URL '/int/WRONG/float/1.1/string/TEST' not found |
31 | | /int/1/float/WRONG/string/TEST | 404 | URL '/int/1/float/WRONG/string/TEST' not found |
32 |
33 | @arguments_by_type
34 | Scenario Outline: Parse form arguments OR from JSON documents for POST / PUT
35 | Given 'arguments' is running
36 |
37 | # pass in as form arguments
38 | When as user 'None:None' I 'http://127.0.0.1:8082/formOrJson' with 'first=John&last=Doe'
39 | Then I expect HTTP code
40 | And I expect content contains 'John Doe'
41 |
42 | # pass in as *** JSON *** document
43 | When as user 'None:None' I 'http://127.0.0.1:8082/formOrJson' with JSON
44 | """
45 | {"first":"Jane","last":"Doeovskaya"}
46 | """
47 | Then I expect HTTP code
48 | And I expect content contains 'Jane Doeovskaya'
49 | # additional arguments should be OK
50 | When as user 'None:None' I 'http://127.0.0.1:8082/formOrJson' with JSON
51 | """
52 | {"first":"Jane","last":"Doeovskaya","middle":"Oksana"}
53 | """
54 | Then I expect HTTP code
55 | And I expect content contains 'Jane Doeovskaya'
56 |
57 | # pass in as *** YAML *** document
58 | When as user 'None:None' I 'http://127.0.0.1:8082/formOrJson' with YAML
59 | """
60 | first: Oksana
61 | last: Dolovskaya
62 | """
63 | Then I expect HTTP code
64 | And I expect content contains 'Oksana Dolovskaya'
65 | # additional arguments should be OK
66 | When as user 'None:None' I 'http://127.0.0.1:8082/formOrJson' with YAML
67 | """
68 | first: Svetlana
69 | middle: Jane
70 | last: Gingrychnoya
71 | """
72 | Then I expect HTTP code
73 | And I expect content contains 'Svetlana Gingrychnoya'
74 |
75 | # pass in as *** XML *** document wit both attributes and child nodes
76 | When as user 'None:None' I 'http://127.0.0.1:8082/formOrJson' with XML
77 | """
78 |
79 | """
80 | Then I expect HTTP code
81 | And I expect content contains 'John Doe'
82 |
83 | When as user 'None:None' I 'http://127.0.0.1:8082/formOrJson' with XML
84 | """
85 |
86 | Dolowski
87 |
88 | """
89 | Then I expect HTTP code
90 | And I expect content contains 'Jan Dolowski'
91 |
92 | When as user 'None:None' I 'http://127.0.0.1:8082/formOrJson' with XML
93 | """
94 |
95 | Grzegorz
96 | Jim
97 | Brzeczyszczykiewicz
98 |
99 | """
100 | Then I expect HTTP code
101 | And I expect content contains 'Grzegorz Brzeczyszczykiewicz'
102 |
103 | Examples:
104 | | method | code |
105 | | POST | 201 |
106 | | PUT | 200 |
107 |
108 | @arguments_by_type
109 | Scenario: Parse query arguments for GET
110 | Given 'arguments' is running
111 | When as user 'None:None' I GET 'http://127.0.0.1:8082/formOrJson?first=John&last=Doe'
112 | Then I expect HTTP code 200
113 | And I expect content contains 'John Doe'
114 |
--------------------------------------------------------------------------------
/corepost/test/feature/content_types.feature:
--------------------------------------------------------------------------------
1 | Using step definitions from: '../steps'
2 |
3 | @content_types
4 | Feature: Content types
5 | CorePost should be able to
6 | correctly parse/generate
7 | JSON/XML/YAML based on content types
8 |
9 | Background:
10 | Given 'home_resource' is running
11 |
12 | @json
13 | Scenario Outline: Parse incoming JSON data
14 | When as user 'None:None' I 'http://127.0.0.1:8080/post/json' with JSON
15 | """
16 | {"test":"test2"}
17 | """
18 | Then I expect HTTP code
19 | And I expect JSON content
20 | """
21 | {"test":"test2"}
22 | """
23 |
24 | Examples:
25 | | method | code |
26 | | POST | 201 |
27 | | PUT | 200 |
28 |
29 | @json
30 | Scenario Outline: Handle invalid incoming JSON data
31 | When as user 'None:None' I 'http://127.0.0.1:8080/post/json' with JSON
32 | """
33 | wrong_json
34 | """
35 | Then I expect HTTP code 400
36 | And I expect content contains 'Unable to parse JSON body: No JSON object could be decoded'
37 |
38 | Examples:
39 | | method |
40 | | POST |
41 | | PUT |
42 |
43 | @xml
44 | Scenario Outline: Parse incoming XML data
45 | When as user 'None:None' I 'http://127.0.0.1:8080/post/xml' with XML
46 | """
47 | TESTYo
48 | """
49 | Then I expect HTTP code
50 | # ElementTree object
51 | And I expect content contains 'TESTYo'
52 |
53 | Examples:
54 | | method | code |
55 | | POST | 201 |
56 | | PUT | 200 |
57 |
58 | @xml
59 | Scenario Outline: Handle invalid XML data
60 | When as user 'None:None' I 'http://127.0.0.1:8080/post/xml' with XML
61 | """
62 | wrong xml
63 | """
64 | Then I expect HTTP code 400
65 | And I expect content contains 'Unable to parse XML body: syntax error: line 1, column 0'
66 |
67 | Examples:
68 | | method |
69 | | POST |
70 | | PUT |
71 |
72 |
73 | @yaml
74 | Scenario Outline: Parse incoming YAML data
75 | When as user 'None:None' I 'http://127.0.0.1:8080/post/yaml' with YAML
76 | """
77 | invoice: 34843
78 | date : 2001-01-23
79 | bill-to: &id001
80 | given : Chris
81 | family : Dumars
82 | address:
83 | lines: |
84 | 458 Walkman Dr.
85 | Suite #292
86 | city : Royal Oak
87 | state : MI
88 | postal : 48046
89 | ship-to: *id001
90 | product:
91 | - sku : BL394D
92 | quantity : 4
93 | description : Basketball
94 | price : 450.00
95 | - sku : BL4438H
96 | quantity : 1
97 | description : Super Hoop
98 | price : 2392.00
99 | tax : 251.42
100 | total: 4443.52
101 | comments: >
102 | Late afternoon is best.
103 | Backup contact is Nancy
104 | Billsmer @ 338-4338.
105 | """
106 | Then I expect HTTP code
107 | And I expect content contains
108 | """
109 | bill-to: &id001
110 | address:
111 | city: Royal Oak
112 | lines: '458 Walkman Dr.
113 |
114 | Suite #292
115 |
116 | '
117 | postal: 48046
118 | state: MI
119 | family: Dumars
120 | given: Chris
121 | comments: Late afternoon is best. Backup contact is Nancy Billsmer @ 338-4338.
122 | date: 2001-01-23
123 | invoice: 34843
124 | product:
125 | - description: Basketball
126 | price: 450.0
127 | quantity: 4
128 | sku: BL394D
129 | - description: Super Hoop
130 | price: 2392.0
131 | quantity: 1
132 | sku: BL4438H
133 | ship-to: *id001
134 | tax: 251.42
135 | total: 4443.52
136 | """
137 |
138 | Examples:
139 | | method | code |
140 | | POST | 201 |
141 | | PUT | 200 |
142 |
143 | @yaml
144 | Scenario Outline: Handle invalid YAML data
145 | When as user 'None:None' I 'http://127.0.0.1:8080/post/yaml' with YAML
146 | """
147 | - test
148 | {test}
149 | """
150 | Then I expect HTTP code 400
151 | And I expect content contains 'Unable to parse YAML body: while scanning a simple key'
152 |
153 | Examples:
154 | | method |
155 | | POST |
156 | | PUT |
157 |
158 | @json @yaml @xml @route_content_type
159 | Scenario Outline: Route by incoming content type
160 | When I prepare HTTP header 'content-type' = ''
161 | When as user 'None:None' I 'http://127.0.0.1:8080/post/by/content' with body ''
162 | Then I expect HTTP code
163 | And I expect content contains ''
164 |
165 | Examples:
166 | | method | type | body | content | code |
167 | | POST | JSON | {"test":2} | application/json | 201 |
168 | | POST | XML | 1 | application/xml | 201 |
169 | | POST | XML | 1 | text/xml | 201 |
170 | | POST | YAML | test: 2 | text/yaml | 201 |
171 | | PUT | JSON | {"test":2} | application/json | 200 |
172 | | PUT | XML | 1 | text/xml | 200 |
173 | | PUT | XML | 1 | application/xml | 200 |
174 | | PUT | YAML | test: 2 | text/yaml | 200 |
175 |
176 | @json @yaml @xml @return_accept
177 | Scenario Outline: Return content type based on caller's Accept
178 | When I prepare HTTP header 'Accept' = ''
179 | When as user 'None:None' I GET 'http://127.0.0.1:8080/return/by/accept'
180 | Then I expect HTTP code
181 | And I expect content contains ''
182 |
183 | Examples:
184 | | content | accept | code |
185 | | [{"test1": "Test1"}, {"test2": "Test2"}] | application/json | 200 |
186 | | - Test1
- Test2
| application/xml | 200 |
187 | | - {test1: Test1}\n- {test2: Test2} | text/yaml | 200 |
188 |
189 | @json @yaml @xml @return_accept_deferred
190 | Scenario Outline: Return content type based on caller's Accept from Deferred methods
191 | When I prepare HTTP header 'Accept' = ''
192 | When as user 'None:None' I GET 'http://127.0.0.1:8080/return/by/accept/deferred'
193 | Then I expect HTTP code
194 | And I expect content contains ''
195 |
196 | Examples:
197 | | content | accept | code |
198 | | [{"test1": "Test1"}, {"test2": "Test2"}] | application/json | 200 |
199 | | - Test1
- Test2
| application/xml | 200 |
200 | | - {test1: Test1}\n- {test2: Test2} | text/yaml | 200 |
201 |
202 | @json @yaml @xml @return_accept @tmp
203 | Scenario Outline: Return class content type based on caller's Accept
204 | When I prepare HTTP header 'Accept' = ''
205 | When as user 'None:None' I GET 'http://127.0.0.1:8080/return/by/accept/class'
206 | Then I expect HTTP code
207 | And I expect content contains ''
208 |
209 | Examples:
210 | | content | accept | code |
211 | | [{"test1": "Test1"}, {"test2": "Test2"}] | application/json | 200 |
212 | | - Test1
- Test2
| application/xml | 200 |
213 | | - {test1: Test1}\n- {test2: Test2} | text/yaml | 200 |
214 |
--------------------------------------------------------------------------------
/corepost/test/feature/filters.feature:
--------------------------------------------------------------------------------
1 | Using step definitions from: '../steps'
2 |
3 | @filters
4 | Feature: Filters
5 | CorePost should be able to
6 | filter incoming requests and outgoing responses
7 |
8 | Background:
9 | Given 'filter_resource' is running
10 |
11 | Scenario: Filter turns 404 into 503
12 | When as user 'None:None' I GET 'http://127.0.0.1:8083/wrongurl'
13 | Then I expect HTTP code 503
14 |
15 | Scenario: Request filter adds a header + wrap around requests
16 | When I prepare HTTP header 'Accept' = 'application/json'
17 | When as user 'None:None' I GET 'http://127.0.0.1:8083/'
18 | Then I expect HTTP code 200
19 | # 'custom-header' should be added
20 | # 'x-wrap-input' should be added from wrap request filter
21 | And I expect JSON content
22 | """
23 | {
24 | "accept": "application/json",
25 | "accept-encoding": "gzip, deflate",
26 | "custom-header": "Custom Header Value",
27 | "host": "127.0.0.1:8083",
28 | "x-wrap-input": "Input"
29 | }
30 | """
31 | # 'x-wrap-header' should be added from wrap response filter
32 | And I expect 'x-wrap-output' header matches 'Output'
33 |
--------------------------------------------------------------------------------
/corepost/test/feature/issues.feature:
--------------------------------------------------------------------------------
1 | Using step definitions from: '../steps'
2 |
3 | @issues
4 | Feature: Issues
5 | Fixes for issues reported on github
6 |
7 | @issue1
8 | Scenario: Issue 1 (unable to access self.var in a router method)
9 | Given 'home_resource' is running
10 | When as user 'None:None' I GET 'http://127.0.0.1:8080/issues/1'
11 | Then I expect HTTP code 200
12 | And I expect content contains 'issue 1'
13 |
14 |
--------------------------------------------------------------------------------
/corepost/test/feature/rest_app.feature:
--------------------------------------------------------------------------------
1 | Using step definitions from: '../steps'
2 |
3 | @rest
4 | Feature: REST App
5 | CorePost should be able to build REST applications
6 | for nested REST resources
7 |
8 | Background:
9 | Given 'rest_resource' is running
10 | # make sure it is empty
11 | When as user 'None:None' I DELETE 'http://127.0.0.1:8085/customer'
12 | Then I expect HTTP code 200
13 | # add a few default customers
14 | When as user 'None:None' I POST 'http://127.0.0.1:8085/customer' with 'customerId=d1&firstName=John&lastName=Doe1'
15 | Then I expect HTTP code 201
16 | When as user 'None:None' I POST 'http://127.0.0.1:8085/customer' with 'customerId=d2&firstName=John&lastName=Doe2'
17 | Then I expect HTTP code 201
18 | When as user 'None:None' I POST 'http://127.0.0.1:8085/customer' with 'customerId=d3&firstName=John&lastName=Doe3'
19 | Then I expect HTTP code 201
20 |
21 |
22 | @customer
23 | Scenario: Full Customer lifecycle
24 | When as user 'None:None' I GET 'http://127.0.0.1:8085/customer'
25 | Then I expect HTTP code 200
26 | And I expect JSON content
27 | """
28 | [
29 | {
30 | "addresses": {},
31 | "customerId": "d2",
32 | "firstName": "John",
33 | "lastName": "Doe2"
34 | },
35 | {
36 | "addresses": {},
37 | "customerId": "d3",
38 | "firstName": "John",
39 | "lastName": "Doe3"
40 | },
41 | {
42 | "addresses": {},
43 | "customerId": "d1",
44 | "firstName": "John",
45 | "lastName": "Doe1"
46 | }
47 | ]
48 | """
49 | # add 1
50 | When as user 'None:None' I POST 'http://127.0.0.1:8085/customer' with 'customerId=c1&firstName=John&lastName=Doe'
51 | Then I expect HTTP code 201
52 | When as user 'None:None' I GET 'http://127.0.0.1:8085/customer/c1'
53 | Then I expect HTTP code 200
54 | And I expect JSON content
55 | """
56 | {
57 | "addresses": {},
58 | "customerId": "c1",
59 | "firstName": "John",
60 | "lastName": "Doe"
61 | }
62 | """
63 | # update
64 | When as user 'None:None' I PUT 'http://127.0.0.1:8085/customer/c1' with 'firstName=Jill&lastName=Jones'
65 | Then I expect HTTP code 200
66 | When as user 'None:None' I GET 'http://127.0.0.1:8085/customer/c1'
67 | Then I expect HTTP code 200
68 | And I expect JSON content
69 | """
70 | {
71 | "addresses": {},
72 | "customerId": "c1",
73 | "firstName": "Jill",
74 | "lastName": "Jones"
75 | }
76 | """
77 | # delete
78 | When as user 'None:None' I DELETE 'http://127.0.0.1:8085/customer/c1'
79 | Then I expect HTTP code 200
80 | When as user 'None:None' I GET 'http://127.0.0.1:8085/customer/c1'
81 | Then I expect HTTP code 404
82 | # delete all
83 | When as user 'None:None' I DELETE 'http://127.0.0.1:8085/customer'
84 | Then I expect HTTP code 200
85 | When as user 'None:None' I GET 'http://127.0.0.1:8085/customer'
86 | Then I expect HTTP code 200
87 | And I expect JSON content
88 | """
89 | []
90 | """
91 |
92 | @customer_address
93 | Scenario: Full Customer Address lifecycle
94 | When as user 'None:None' I GET 'http://127.0.0.1:8085/customer/d1/address'
95 | Then I expect HTTP code 200
96 | And I expect JSON content
97 | """
98 | {}
99 | """
100 | # add 1
101 | 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'
102 | Then I expect HTTP code 201
103 | # get just the address
104 | When as user 'None:None' I GET 'http://127.0.0.1:8085/customer/d1/address/HOME'
105 | Then I expect HTTP code 200
106 | And I expect JSON content
107 | """
108 | {
109 | "countryCode": "US",
110 | "stateCode": "CA",
111 | "streetName": "MyStreet",
112 | "streetNumber": "100"
113 | }
114 | """
115 | # get the customer with the address
116 | When as user 'None:None' I GET 'http://127.0.0.1:8085/customer/d1'
117 | Then I expect HTTP code 200
118 | And I expect JSON content
119 | """
120 | {
121 | "addresses": {
122 | "HOME": {
123 | "countryCode": "US",
124 | "stateCode": "CA",
125 | "streetName": "MyStreet",
126 | "streetNumber": "100"
127 | }
128 | },
129 | "customerId": "d1",
130 | "firstName": "John",
131 | "lastName": "Doe1"
132 | }
133 | """
134 | # update address
135 | 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'
136 | Then I expect HTTP code 200
137 | When as user 'None:None' I GET 'http://127.0.0.1:8085/customer/d1/address/HOME'
138 | Then I expect HTTP code 200
139 | And I expect JSON content
140 | """
141 | {
142 | "countryCode": "US",
143 | "stateCode": "CA",
144 | "streetName": "MyStreet2",
145 | "streetNumber": "1002"
146 | }
147 | """
148 | # delete address
149 | When as user 'None:None' I DELETE 'http://127.0.0.1:8085/customer/d1/address/HOME'
150 | Then I expect HTTP code 200
151 | When as user 'None:None' I GET 'http://127.0.0.1:8085/customer/d1/address/HOME'
152 | Then I expect HTTP code 404
153 |
154 | @customer @xml @issue4
155 | Scenario: Customers as XML (Issue #4)
156 | # all
157 | When I prepare HTTP header 'Accept' = 'application/xml'
158 | When as user 'None:None' I GET 'http://127.0.0.1:8085/customer'
159 | Then I expect HTTP code 200
160 | Then I expect content contains '- Doe2d2John
- Doe3d3John
- Doe1d1John
'
161 | # 1
162 | When I prepare HTTP header 'Accept' = 'application/xml'
163 | When as user 'None:None' I GET 'http://127.0.0.1:8085/customer/d2'
164 | Then I expect HTTP code 200
165 | Then I expect content contains '- Doe2d2John
'
166 | # 1 with address
167 | When as user 'None:None' I POST 'http://127.0.0.1:8085/customer/d2/address' with 'addressId=HOME&streetNumber=100&streetName=MyStreet&stateCode=CA&countryCode=US'
168 | When I prepare HTTP header 'Accept' = 'application/xml'
169 | When as user 'None:None' I GET 'http://127.0.0.1:8085/customer/d2'
170 | Then I expect HTTP code 200
171 | Then I expect content contains '- Doe2d2JohnUSMyStreet100CA
'
172 |
--------------------------------------------------------------------------------
/corepost/test/feature/url_routing.feature:
--------------------------------------------------------------------------------
1 | Using step definitions from: '../steps'
2 |
3 | @url_routing
4 | Feature: URL routing
5 | CorePost should be able to
6 | correctly route requests
7 | depending on how the Resource instances
8 | were registered
9 |
10 | @single @single_get
11 | Scenario: Single resource - GET
12 | Given 'home_resource' is running
13 | When as user 'None:None' I GET 'http://127.0.0.1:8080'
14 | Then I expect HTTP code 200
15 | And I expect content contains '{}'
16 | When as user 'None:None' I GET 'http://127.0.0.1:8080/?test=value'
17 | Then I expect HTTP code 200
18 | And I expect content contains '{'test': 'value'}'
19 | When as user 'None:None' I GET 'http://127.0.0.1:8080/test?query=test'
20 | Then I expect HTTP code 200
21 | And I expect content contains '{'query': 'test'}'
22 | When as user 'None:None' I GET 'http://127.0.0.1:8080/test/23/resource/someid'
23 | Then I expect HTTP code 200
24 | And I expect content contains '23 - someid'
25 |
26 | @single @single_post
27 | Scenario: Single resource - POST
28 | Given 'home_resource' is running
29 | When as user 'None:None' I POST 'http://127.0.0.1:8080/post' with 'test=value&test2=value2'
30 | Then I expect HTTP code 201
31 | And I expect content contains '{'test': 'value', 'test2': 'value2'}'
32 |
33 | @single @single_put
34 | Scenario: Single resource - PUT
35 | Given 'home_resource' is running
36 | When as user 'None:None' I PUT 'http://127.0.0.1:8080/put' with 'test=value&test2=value2'
37 | Then I expect HTTP code 200
38 | And I expect content contains '{'test': 'value', 'test2': 'value2'}'
39 |
40 | @single @single_delete
41 | Scenario: Single resource - DELETE
42 | Given 'home_resource' is running
43 | When as user 'None:None' I DELETE 'http://127.0.0.1:8080/delete'
44 | Then I expect HTTP code 200
45 |
46 | @single @single_post @single_put
47 | Scenario: Single resource - multiple methods at same URL
48 | Given 'home_resource' is running
49 | When as user 'None:None' I POST 'http://127.0.0.1:8080/postput' with 'test=value&test2=value2'
50 | # POST return 201 by default
51 | Then I expect HTTP code 201
52 | And I expect content contains '{'test': 'value', 'test2': 'value2'}'
53 | When as user 'None:None' I PUT 'http://127.0.0.1:8080/postput' with 'test=value&test3=value3'
54 | # PUT return 200 by default
55 | Then I expect HTTP code 200
56 | And I expect content contains '{'test': 'value', 'test3': 'value3'}'
57 |
58 | @multi
59 | Scenario Outline: Multiple resources with submodules
60 | Given 'multi_resource' is running
61 | When as user 'None:None' I GET ''
62 | Then I expect HTTP code 200
63 |
64 | Examples:
65 | | url |
66 | | http://127.0.0.1:8081 |
67 | | http://127.0.0.1:8081/ |
68 | | http://127.0.0.1:8081/module1 |
69 | | http://127.0.0.1:8081/module1/ |
70 | | http://127.0.0.1:8081/module1/sub |
71 | | http://127.0.0.1:8081/module2 |
72 | | http://127.0.0.1:8081/module2/ |
73 | | http://127.0.0.1:8081/module2/sub |
74 |
75 | @501
76 | Scenario: Existing URLs with wrong HTTP method returns 501 error
77 | Given 'home_resource' is running
78 | When as user 'None:None' I DELETE 'http://127.0.0.1:8080/postput'
79 | Then I expect HTTP code 501
80 | When as user 'None:None' I GET 'http://127.0.0.1:8080/postput'
81 | Then I expect HTTP code 501
82 |
83 | @head
84 | Scenario: Support for HTTP HEAD
85 | Given 'home_resource' is running
86 | When as user 'None:None' I GET 'http://127.0.0.1:8080/methods/head'
87 | Then I expect HTTP code 501
88 | When as user 'None:None' I HEAD 'http://127.0.0.1:8080/methods/head'
89 | Then I expect HTTP code 200
90 |
91 | @options
92 | Scenario: TODO: Support for HTTP OPTIONS
93 | Given 'home_resource' is running
94 | When as user 'None:None' I GET 'http://127.0.0.1:8080/methods/options'
95 | Then I expect HTTP code 501
96 | When as user 'None:None' I OPTIONS 'http://127.0.0.1:8080/methods/options'
97 | #this is unexpected - need to verify with kaosat
98 | Then I expect HTTP code 501
99 |
100 | @patch
101 | Scenario: TODO: Support for HTTP PATCH
102 | Given 'home_resource' is running
103 | When as user 'None:None' I GET 'http://127.0.0.1:8080/methods/options'
104 | Then I expect HTTP code 501
105 | #this is unexpected - need to verify with kaosat
106 | When as user 'None:None' I PATCH 'http://127.0.0.1:8080/methods/patch' with 'tes1=value1&test2=value2'
107 | Then I expect HTTP code 501
108 |
109 |
110 |
--------------------------------------------------------------------------------
/corepost/test/feature/validate.feature:
--------------------------------------------------------------------------------
1 | Using step definitions from: '../steps'
2 |
3 | @validate
4 | Feature: Argument Validators
5 | CorePost should be able to correctly validate path, query and form arguments
6 |
7 | @validate
8 | Scenario Outline: Form argument validation
9 | Given 'arguments' is running
10 | # childId accepts only jacekf or test, via Regex validator
11 | When as user 'None:None' I POST 'http://127.0.0.1:8082/validate/23/' with ''
12 | Then I expect HTTP code
13 | And I expect content contains ''
14 |
15 | Examples:
16 | | url | args | code | content |
17 | # validates using argument-specific validators
18 | | custom | childId=jacekf | 201 | 23 - jacekf - {} |
19 | | custom | childId=jacekf&otherId=test | 201 | 23 - jacekf - {'otherId': 'test'} |
20 | | custom | childId=test | 201 | 23 - test - {} |
21 | | custom | childId=wrong | 400 | childId: The input is not valid ('wrong') |
22 | # validates using Schema
23 | | schema | childId=jacekf | 201 | 23 - jacekf - {} |
24 | | schema | childId=jacekf&otherId=test | 201 | 23 - jacekf - {'otherId': 'test'} |
25 | | schema | childId=test | 201 | 23 - test - {} |
26 | | schema | childId=wrong | 400 | childId: The input is not valid ('wrong') |
27 |
--------------------------------------------------------------------------------
/corepost/test/feature/zeromq_resource.py:
--------------------------------------------------------------------------------
1 | '''
2 | ZeroMQ resource
3 |
4 | @author: jacekf
5 | '''
6 |
7 | from corepost.web import RESTResource, route
8 | from corepost.enums import Http
9 | from corepost.filters import IRequestFilter, IResponseFilter
10 | from zope.interface import implements
11 |
12 | from multiprocessing import Pool
13 |
14 | class TestService:
15 |
16 | @route("/")
17 | def forward(self,request):
18 | return ""
19 |
20 | def startClient():
21 | return "TEST"
22 |
23 |
24 | def run_app_multicore():
25 | #start the ZeroMQ client
26 | pool = Pool(processes=4)
27 |
28 | #start the server
29 | app = RESTResource((TestService(),))
30 | app.run(8090)
31 |
32 | if __name__ == "__main__":
33 | run_app_multicore()
34 |
35 |
--------------------------------------------------------------------------------
/corepost/test/filter_resource.py:
--------------------------------------------------------------------------------
1 | '''
2 | Server tests
3 | @author: jacekf
4 | '''
5 |
6 | from corepost.web import RESTResource, route
7 | from corepost.enums import Http
8 | from corepost.filters import IRequestFilter, IResponseFilter
9 | from zope.interface import implements
10 |
11 | class AddCustomHeaderFilter():
12 | """Implements just a request filter"""
13 | implements(IRequestFilter)
14 |
15 | def filterRequest(self,request):
16 | request.received_headers["Custom-Header"] = "Custom Header Value"
17 |
18 | class Change404to503Filter():
19 | """Implements just a response filter that changes 404 to 503 statuses"""
20 | implements(IResponseFilter)
21 |
22 | def filterResponse(self,request,response):
23 | if response.code == 404:
24 | response.code = 503
25 |
26 | class WrapAroundFilter():
27 | """Implements both types of filters in one class"""
28 | implements(IRequestFilter,IResponseFilter)
29 |
30 | def filterRequest(self,request):
31 | del(request.received_headers["user-agent"]) # remove this for unit tests, it varies from one box to another
32 | request.received_headers["X-Wrap-Input"] = "Input"
33 |
34 | def filterResponse(self,request,response):
35 | response.headers["X-Wrap-Output"] = "Output"
36 |
37 | class FilterService():
38 | path = "/"
39 |
40 | @route("/",Http.GET)
41 | def root(self,request,**kwargs):
42 | return request.received_headers
43 |
44 | def run_filter_app():
45 | app = RESTResource(services=(FilterService(),),filters=(Change404to503Filter(),AddCustomHeaderFilter(),WrapAroundFilter(),))
46 | app.run(8083)
47 |
48 | if __name__ == "__main__":
49 | run_filter_app()
--------------------------------------------------------------------------------
/corepost/test/home_resource.py:
--------------------------------------------------------------------------------
1 | '''
2 | Server tests
3 | @author: jacekf
4 | '''
5 |
6 | from corepost.web import RESTResource, route
7 | from corepost.enums import Http, MediaType, HttpHeader
8 | from twisted.internet import defer
9 | from xml.etree import ElementTree
10 | import json, yaml
11 |
12 | class HomeApp():
13 |
14 | def __init__(self,*args,**kwargs):
15 | self.issue1 = "issue 1"
16 |
17 | @route("/",Http.GET)
18 | @defer.inlineCallbacks
19 | def root(self,request,**kwargs):
20 | yield 1
21 | request.write("%s" % kwargs)
22 | request.finish()
23 |
24 | @route("/test",Http.GET)
25 | def test(self,request,**kwargs):
26 | return "%s" % kwargs
27 |
28 | @route("/test//resource/",Http.GET)
29 | def test_get_resources(self,request,numericid,stringid,**kwargs):
30 | return "%s - %s" % (numericid,stringid)
31 |
32 | @route("/post",(Http.POST,Http.PUT))
33 | def test_post(self,request,**kwargs):
34 | return "%s" % kwargs
35 |
36 | @route("/put",(Http.POST,Http.PUT))
37 | def test_put(self,request,**kwargs):
38 | return "%s" % kwargs
39 |
40 | @route("/postput",(Http.POST,Http.PUT))
41 | def test_postput(self,request,**kwargs):
42 | return "%s" % kwargs
43 |
44 | @route("/delete",Http.DELETE)
45 | def test_delete(self,request,**kwargs):
46 | return "%s" % kwargs
47 |
48 | @route("/post/json",(Http.POST,Http.PUT))
49 | def test_json(self,request,**kwargs):
50 | return "%s" % json.dumps(request.json)
51 |
52 | @route("/post/xml",(Http.POST,Http.PUT))
53 | def test_xml(self,request,**kwargs):
54 | return "%s" % ElementTree.tostring(request.xml)
55 |
56 | @route("/post/yaml",(Http.POST,Http.PUT))
57 | def test_yaml(self,request,**kwargs):
58 | return "%s" % yaml.dump(request.yaml,indent=4,width=130,default_flow_style=False)
59 |
60 | ##################################################################
61 | # same URLs, routed by incoming content type
62 | ###################################################################
63 | @route("/post/by/content",(Http.POST,Http.PUT),MediaType.APPLICATION_JSON)
64 | def test_content_app_json(self,request,**kwargs):
65 | return request.received_headers[HttpHeader.CONTENT_TYPE]
66 |
67 | @route("/post/by/content",(Http.POST,Http.PUT),(MediaType.TEXT_XML,MediaType.APPLICATION_XML))
68 | def test_content_xml(self,request,**kwargs):
69 | return request.received_headers[HttpHeader.CONTENT_TYPE]
70 |
71 | @route("/post/by/content",(Http.POST,Http.PUT),MediaType.TEXT_YAML)
72 | def test_content_yaml(self,request,**kwargs):
73 | return request.received_headers[HttpHeader.CONTENT_TYPE]
74 |
75 | @route("/post/by/content",(Http.POST,Http.PUT))
76 | def test_content_catch_all(self,request,**kwargs):
77 | return MediaType.WILDCARD
78 |
79 | ##################################################################
80 | # one URL, serving different content types
81 | ###################################################################
82 | @route("/return/by/accept")
83 | def test_return_content_by_accepts(self,request,**kwargs):
84 | val = [{"test1":"Test1"},{"test2":"Test2"}]
85 | return val
86 |
87 | @route("/return/by/accept/deferred")
88 | @defer.inlineCallbacks
89 | def test_return_content_by_accept_deferred(self,request,**kwargs):
90 | """Ensure support for inline callbacks and deferred"""
91 | val = yield [{"test1":"Test1"},{"test2":"Test2"}]
92 | defer.returnValue(val)
93 |
94 | @route("/return/by/accept/class")
95 | def test_return_class_content_by_accepts(self,request,**kwargs):
96 | """Uses Python class instead of dict/list"""
97 |
98 | class TestReturn:
99 | """Test return class"""
100 | def __init__(self):
101 | self.__t1 = 'Test'
102 |
103 | t1 = TestReturn()
104 | t1.test1 = 'Test1'
105 |
106 | t2 = TestReturn()
107 | t2.test2="Test2"
108 | return (t1,t2)
109 |
110 | ####################################
111 | # Issues
112 | ####################################
113 | @route("/issues/1")
114 | def test_issue_1(self,request,**kwargs):
115 | return self.issue1
116 |
117 | ####################################
118 | # extra HTTP methods
119 | ####################################
120 | @route("/methods/head",Http.HEAD)
121 | def test_head_http(self,request,**kwargs):
122 | return ""
123 |
124 | @route("/methods/options",Http.OPTIONS)
125 | def test_options_http(self,request,**kwargs):
126 | return "OPTIONS"
127 |
128 | @route("/methods/patch",Http.PATCH)
129 | def test_patch_http(self,request,**kwargs):
130 | return "PATCH=%s" % kwargs
131 |
132 | def run_app_home():
133 | app = RESTResource((HomeApp(),))
134 | app.run()
135 |
136 | if __name__ == "__main__":
137 | run_app_home()
--------------------------------------------------------------------------------
/corepost/test/multi_resource.py:
--------------------------------------------------------------------------------
1 | '''
2 | A RESTResource module1 that can be merged into the main RESTResource Resource
3 | '''
4 |
5 | from corepost.web import RESTResource, route
6 | from corepost.enums import Http
7 |
8 | class HomeApp():
9 |
10 | @route("/")
11 | def home_root(self,request,**kwargs):
12 | return "HOME %s" % kwargs
13 |
14 | class Module1():
15 | path = "/module1"
16 |
17 | @route("/",Http.GET)
18 | def module1_get(self,request,**kwargs):
19 | return request.path
20 |
21 | @route("/sub",Http.GET)
22 | def module1e_sub(self,request,**kwargs):
23 | return request.path
24 |
25 | class Module2():
26 | path = "/module2"
27 |
28 | @route("/",Http.GET)
29 | def module2_get(self,request,**kwargs):
30 | return request.path
31 |
32 | @route("/sub",Http.GET)
33 | def module2_sub(self,request,**kwargs):
34 | return request.path
35 |
36 | def run_app_multi():
37 | app = RESTResource((HomeApp(),Module1(),Module2()))
38 | app.run(8081)
39 |
40 | if __name__ == "__main__":
41 | run_app_multi()
--------------------------------------------------------------------------------
/corepost/test/rest_resource.py:
--------------------------------------------------------------------------------
1 | '''
2 | Server tests
3 | @author: jacekf
4 | '''
5 |
6 | from corepost import Response, NotFoundException, AlreadyExistsException
7 | from corepost.web import RESTResource, route, Http
8 |
9 | from twisted.cred.portal import IRealm, Portal
10 | from twisted.cred.checkers import FilePasswordDB
11 | from twisted.web.static import File
12 | from twisted.web.resource import IResource
13 | from twisted.web.guard import HTTPAuthSessionWrapper, BasicCredentialFactory
14 |
15 | from zope.interface import implements
16 |
17 | # Security
18 |
19 | # Database
20 | class DB():
21 | """Fake in-memory DB for testing"""
22 | customers = {}
23 |
24 | @classmethod
25 | def getAllCustomers(cls):
26 | return DB.customers.values()
27 |
28 | @classmethod
29 | def getCustomer(cls,customerId):
30 | if customerId in DB.customers:
31 | return DB.customers[customerId]
32 | else:
33 | raise NotFoundException("Customer",customerId)
34 |
35 | @classmethod
36 | def saveCustomer(cls,customer):
37 | if customer.customerId in DB.customers:
38 | raise AlreadyExistsException("Customer",customer.customerId)
39 | else:
40 | DB.customers[customer.customerId] = customer
41 |
42 | @classmethod
43 | def deleteCustomer(cls,customerId):
44 | if customerId in DB.customers:
45 | del(DB.customers[customerId])
46 | else:
47 | raise NotFoundException("Customer",customerId)
48 |
49 | @classmethod
50 | def deleteAllCustomers(cls):
51 | DB.customers.clear()
52 |
53 | @classmethod
54 | def getCustomerAddress(cls,customerId,addressId):
55 | c = DB.getCustomer(customerId)
56 | if addressId in c.addresses:
57 | return c.addresses[addressId]
58 | else:
59 | raise NotFoundException("Customer Address",addressId)
60 |
61 |
62 | class Customer:
63 | """Represents customer entity"""
64 | def __init__(self,customerId,firstName,lastName):
65 | (self.customerId,self.firstName,self.lastName) = (customerId,firstName,lastName)
66 | self.addresses = {}
67 |
68 | class CustomerAddress:
69 | """Represents customer address entity"""
70 | def __init__(self,streetNumber,streetName,stateCode,countryCode):
71 | (self.streetNumber,self.streetName,self.stateCode,self.countryCode) = (streetNumber,streetName,stateCode,countryCode)
72 |
73 | class CustomerRESTService():
74 | path = "/customer"
75 |
76 | @route("/")
77 | def getAll(self,request):
78 | return DB.getAllCustomers()
79 |
80 | @route("/")
81 | def get(self,request,customerId):
82 | return DB.getCustomer(customerId)
83 |
84 | @route("/",Http.POST)
85 | def post(self,request,customerId,firstName,lastName):
86 | customer = Customer(customerId, firstName, lastName)
87 | DB.saveCustomer(customer)
88 | return Response(201)
89 |
90 | @route("/",Http.PUT)
91 | def put(self,request,customerId,firstName,lastName):
92 | c = DB.getCustomer(customerId)
93 | (c.firstName,c.lastName) = (firstName,lastName)
94 | return Response(200)
95 |
96 | @route("/",Http.DELETE)
97 | def delete(self,request,customerId):
98 | DB.deleteCustomer(customerId)
99 | return Response(200)
100 |
101 | @route("/",Http.DELETE)
102 | def deleteAll(self,request):
103 | DB.deleteAllCustomers()
104 | return Response(200)
105 |
106 | class CustomerAddressRESTService():
107 | path = "/customer//address"
108 |
109 | @route("/")
110 | def getAll(self,request,customerId):
111 | return DB.getCustomer(customerId).addresses
112 |
113 | @route("/")
114 | def get(self,request,customerId,addressId):
115 | return DB.getCustomerAddress(customerId, addressId)
116 |
117 | @route("/",Http.POST)
118 | def post(self,request,customerId,addressId,streetNumber,streetName,stateCode,countryCode):
119 | c = DB.getCustomer(customerId)
120 | address = CustomerAddress(streetNumber,streetName,stateCode,countryCode)
121 | c.addresses[addressId] = address
122 | return Response(201)
123 |
124 | @route("/",Http.PUT)
125 | def put(self,request,customerId,addressId,streetNumber,streetName,stateCode,countryCode):
126 | address = DB.getCustomerAddress(customerId, addressId)
127 | (address.streetNumber,address.streetName,address.stateCode,address.countryCode) = (streetNumber,streetName,stateCode,countryCode)
128 | return Response(200)
129 |
130 | @route("/",Http.DELETE)
131 | def delete(self,request,customerId,addressId):
132 | DB.getCustomerAddress(customerId, addressId) #validate address exists
133 | del(DB.getCustomer(customerId).addresses[addressId])
134 | return Response(200)
135 |
136 | @route("/",Http.DELETE)
137 | def deleteAll(self,request,customerId):
138 | c = DB.getCustomer(customerId)
139 | c.addresses = {}
140 | return Response(200)
141 |
142 | def run_rest_app():
143 | app = RESTResource((CustomerRESTService(),CustomerAddressRESTService()))
144 | app.run(8085)
145 |
146 | if __name__ == "__main__":
147 | run_rest_app()
--------------------------------------------------------------------------------
/corepost/test/sql_resource.py:
--------------------------------------------------------------------------------
1 | '''
2 | Created on 2012-04-17
3 |
4 | @author: jacekf
5 | '''
6 | from corepost.web import route
7 | from twisted.python.constants import NamedConstant, Names
8 |
9 | class REST_METHOD(Names):
10 | GET_ALL = NamedConstant()
11 | GET_ONE = NamedConstant()
12 | POST = NamedConstant()
13 | PUT = NamedConstant()
14 | DELETE = NamedConstant()
15 | DELETE_ALL = NamedConstant()
16 | ALL = NamedConstant()
17 |
18 | class DatabaseRegistry:
19 |
20 | __registry = {}
21 |
22 | @classmethod
23 | def getConnection(cls,name=None):
24 | return DatabaseRegistry.__registery[name]
25 |
26 | @classmethod
27 | def registerPool(cls,name,dbPool):
28 | """Registers a DB connection pool under an appropriate name"""
29 | DatabaseRegistry.__registry[name] = dbPool
30 |
31 | @classmethod
32 | def getManager(cls,name=None,queriesFile=None):
33 | """Returns the high-level SQL data manager for easy SQL manipulation"""
34 | pass
35 |
36 | class SqlDataManager:
37 |
38 | def __init__(self,table,columnMapping={}):
39 | pass
40 |
41 | class CustomerSqlService:
42 | path = "/customer"
43 | entityId =""
44 | dataManager = DatabaseRegistry.getManager("customer")
45 | methods = (REST_METHOD.GET_ONE,REST_METHOD.POST,REST_METHOD.PUT,REST_METHOD.DELETE)
46 |
47 | class CustomerAddressSqlService:
48 | path = "/customer//address"
49 | entityId = ""
50 | dataManager = DatabaseRegistry.getManager("customer_address")
51 | methods = (REST_METHOD.ALL,)
52 |
53 |
--------------------------------------------------------------------------------
/corepost/test/steps.py:
--------------------------------------------------------------------------------
1 | '''
2 | Common Freshen BDD steps
3 |
4 | @author: jacekf
5 | '''
6 | from multiprocessing import Process
7 | import httplib2, json, re, time, string
8 | from freshen import Before, Given, When, Then, scc, glc, assert_equals, assert_true #@UnresolvedImport
9 | from urllib import urlencode
10 | from corepost.test.home_resource import run_app_home
11 | from corepost.test.multi_resource import run_app_multi
12 | from corepost.test.arguments import run_app_arguments
13 | from corepost.test.filter_resource import run_filter_app
14 | from corepost.test.rest_resource import run_rest_app
15 |
16 | apps = {'home_resource' : run_app_home,'multi_resource':run_app_multi,'arguments':run_app_arguments, 'filter_resource':run_filter_app,'rest_resource':run_rest_app}
17 |
18 | NULL = 'None'
19 |
20 | def as_dict(parameters):
21 | dict_val = {}
22 | for pair in parameters.split('&') :
23 | params = pair.split('=', 1)
24 | if (params[0] != None) and (len(params) == 2):
25 | dict_val[params[0]] = params[1]
26 | return dict_val
27 |
28 | ##################################
29 | # BEFORE / AFTER
30 | ##################################
31 |
32 | @Before
33 | def setup(slc):
34 | scc.http_headers = {}
35 |
36 | ##################################
37 | # GIVEN
38 | ##################################
39 |
40 | @Given(r"^'(.+)' is running\s*$")
41 | def given_process_is_running(processname):
42 | if glc.processes == None:
43 | glc.processes = {}
44 |
45 | if processname not in glc.processes:
46 | # start a process only once, keep it running
47 | # to make test runs faster
48 | process = Process(target=apps[processname])
49 | process.daemon = True
50 | process.start()
51 | time.sleep(0.25) # let it start up
52 | glc.processes[processname] = process
53 |
54 | ##################################
55 | # WHEN
56 | ##################################
57 |
58 | @When(r"^as user '(.+):(.+)' I (GET|DELETE|HEAD|OPTIONS) '(.+)'\s*$")
59 | def when_as_user_i_send_get_delete_to_url(user,password,method,url):
60 | h = httplib2.Http()
61 | h.follow_redirects = False
62 | h.add_credentials(user, password)
63 | scc.response, scc.content = h.request(url, method, headers = scc.http_headers)
64 |
65 | @When(r"^as user '(.+):(.+)' I (POST|PUT|PATCH) '(.+)' with '(.+)'\s*$")
66 | def when_as_user_i_send_post_put_to_url(user,password,method,url,params):
67 | h = httplib2.Http()
68 | h.follow_redirects = False
69 | h.add_credentials(user, password)
70 | scc.http_headers['Content-type'] = 'application/x-www-form-urlencoded'
71 | scc.response, scc.content = h.request(url, method, urlencode(as_dict(params)), headers = scc.http_headers)
72 |
73 | @When(r"^as user '(.+):(.+)' I (POST|PUT) '(.+)' with (XML|JSON|YAML) body '(.+)'\s*$")
74 | def when_as_user_i_send_post_put_xml_json_to_url(user,password,method,url,request_type,body):
75 | when_as_user_i_send_post_put_xml_json_to_url_multiline(body,user,password,method,url,request_type)
76 |
77 | @When(r"^as user '(.+):(.+)' I (POST|PUT) '(.+)' with (XML|JSON|YAML)\s*$")
78 | def when_as_user_i_send_post_put_xml_json_to_url_multiline(body,user,password,method,url,request_type):
79 | h = httplib2.Http()
80 | h.follow_redirects = False
81 | h.add_credentials(user, password)
82 | if request_type == "JSON":
83 | scc.http_headers['Content-type'] = 'application/json'
84 | elif request_type == "XML":
85 | scc.http_headers['Content-type'] = 'text/xml'
86 | elif request_type == "YAML":
87 | scc.http_headers['Content-type'] = 'text/yaml'
88 | scc.response, scc.content = h.request(url, method, body, headers = scc.http_headers)
89 |
90 | @When("I prepare HTTP header '(.*)' = '(.*)'")
91 | def when_i_define_http_header_with_value(header,value):
92 | if header != NULL:
93 | scc.http_headers[header] = value
94 |
95 | ##################################
96 | # THEN
97 | ##################################
98 | def transform_content(content):
99 | """Support embedded newlines"""
100 | if content != None:
101 | return string.replace(content,"\\n","\n")
102 | else:
103 | return None
104 |
105 | @Then(r"^I expect HTTP code (\d+)\s*$")
106 | def expect_http_code(code):
107 | assert_equals(int(code),int(scc.response.status), msg="%s != %s\n%s\n%s" % (code,scc.response.status,scc.response,scc.content))
108 |
109 | @Then(r"^I expect content contains '(.+)'\s*$")
110 | def expect_content(content):
111 | content = transform_content(content)
112 | assert_true(scc.content.find(content) >= 0,"Did not find:\n%s\nin content:\n%s" % (content,scc.content))
113 |
114 | @Then(r"^I expect content contains\s*$")
115 | def expect_content_multiline(content):
116 | content = transform_content(content)
117 | assert_true(scc.content.find(content) >= 0,"Did not find:\n%s\nin content:\n%s" % (content,scc.content))
118 |
119 | @Then(r"^I expect '([^']*)' header matches '([^']*)'\s*$")
120 | def then_check_http_header_matches(header,regex):
121 | assert_true(re.search(regex,scc.response[header.lower()], re.X | re.I) != None,
122 | "the regex %s does not match the response\n%s" % (regex, scc.response[header.lower()]))
123 |
124 | @Then("^I expect JSON content\s*$")
125 | def then_i_expect_json(content):
126 | expected_json = json.loads(content)
127 | expected_json_sorted = json.dumps(expected_json,sort_keys=True,indent=4)
128 | received_json = json.loads(scc.content)
129 | received_json_sorted = json.dumps(received_json,sort_keys=True,indent=4)
130 | assert_equals(expected_json_sorted,received_json_sorted,"Expected JSON\n%s\n*** actual ****\n%s" % (expected_json_sorted,received_json_sorted))
131 |
132 |
--------------------------------------------------------------------------------
/corepost/utils.py:
--------------------------------------------------------------------------------
1 | '''
2 | Various CorePost utilities
3 | '''
4 | from inspect import getargspec
5 |
6 |
7 | def getMandatoryArgumentNames(f):
8 | '''Returns a tuple of the mandatory arguments required in a function'''
9 | args,_,_,defaults = getargspec(f)
10 | if defaults == None:
11 | return args
12 | else:
13 | return args[0:len(args) - len(defaults)]
14 |
15 |
16 | def getRouterKey(method,url):
17 | '''Returns the common key used to represent a function that a request can be routed to'''
18 | return "%s %s" % (method,url)
19 |
20 |
21 | def checkExpectedInterfaces(objects,expectedInterface):
22 | """Verifies that all the objects implement the expected interface"""
23 | for obj in objects:
24 | if not expectedInterface.providedBy(obj):
25 | raise RuntimeError("Object %s does not implement %s interface" % (obj,expectedInterface))
26 |
27 | def safeDictUpdate(dictObject,key,value):
28 | """Only adds a key to a dictionary. If key exists, it leaves it untouched"""
29 | if key not in dictObject:
30 | dictObject[key] = value
31 |
--------------------------------------------------------------------------------
/corepost/web.py:
--------------------------------------------------------------------------------
1 | '''
2 | Main server classes
3 |
4 | @author: jacekf
5 | '''
6 | from corepost import Response, IRESTResource
7 | from corepost.enums import Http
8 | from corepost.routing import UrlRouter, RequestRouter
9 | from enums import MediaType
10 | from formencode import FancyValidator, Invalid
11 | from twisted.internet import reactor
12 | from twisted.internet.defer import Deferred
13 | from twisted.web.resource import Resource
14 | from twisted.web.server import Site, NOT_DONE_YET
15 | from zope.interface import implements
16 |
17 | #########################################################
18 | #
19 | # CLASSES
20 | #
21 | #########################################################
22 |
23 | class RESTResource(Resource):
24 | '''
25 | Main resource responsible for routing REST requests to the implementing methods
26 | '''
27 | isLeaf = True
28 | implements(IRESTResource)
29 |
30 | def __init__(self,services=(),schema=None,filters=()):
31 | '''
32 | Constructor
33 | '''
34 | self.services = services
35 | self.__router = RequestRouter(self,schema,filters)
36 | Resource.__init__(self)
37 |
38 | def render_GET(self,request):
39 | """ Handles all GET requests """
40 | return self.__renderUrl(request)
41 |
42 | def render_POST(self,request):
43 | """ Handles all POST requests"""
44 | return self.__renderUrl(request)
45 |
46 | def render_PUT(self,request):
47 | """ Handles all PUT requests"""
48 | return self.__renderUrl(request)
49 |
50 | def render_DELETE(self,request):
51 | """ Handles all DELETE requests"""
52 | return self.__renderUrl(request)
53 |
54 | def __renderUrl(self,request):
55 | try:
56 | val = self.__router.getResponse(request)
57 |
58 | # return can be Deferred or Response
59 | if isinstance(val,Deferred):
60 | val.addCallback(self.__finishRequest,request)
61 | return NOT_DONE_YET
62 | elif isinstance(val,Response):
63 | self.__applyResponse(request, val.code, val.headers)
64 | return val.entity
65 | else:
66 | raise RuntimeError("Unexpected return type from request router %s" % val)
67 | except Exception as ex:
68 | self.__applyResponse(request, 500, None)
69 | return str(ex)
70 |
71 | def __finishRequest(self,response,request):
72 | if not request.finished:
73 | self.__applyResponse(request, response.code,response.headers)
74 | request.write(response.entity)
75 | request.finish()
76 |
77 | def __applyResponse(self,request,code,headers={"content-type":MediaType.TEXT_PLAIN}):
78 | request.setResponseCode(code)
79 | if headers != None:
80 | for header,value in headers.iteritems():
81 | request.setHeader(header, value)
82 |
83 | def run(self,port=8080):
84 | """Shortcut for running app within Twisted reactor"""
85 | factory = Site(self)
86 | reactor.listenTCP(port, factory) #@UndefinedVariable
87 | reactor.run() #@UndefinedVariable
88 |
89 | ##################################################################################################
90 | #
91 | # DECORATORS
92 | #
93 | ##################################################################################################
94 |
95 | def route(url,methods=(Http.GET,),accepts=MediaType.WILDCARD,produces=None,cache=True):
96 | '''
97 | Main decorator for registering REST functions
98 | '''
99 | def decorator(f):
100 | def wrap(*args,**kwargs):
101 | return f
102 | router = UrlRouter(f, url, methods, accepts, produces, cache)
103 | setattr(wrap,'corepostRequestRouter',router)
104 |
105 | return wrap
106 | return decorator
107 |
108 | def validate(schema=None,**vKwargs):
109 | '''
110 | Main decorator for registering additional validators for incoming URL arguments
111 | '''
112 | def fn(realfn):
113 | def wrap(*args,**kwargs):
114 | # first run schema validation, then the custom validators
115 | errors = []
116 | if schema != None:
117 | try:
118 | schema.to_python(kwargs)
119 | except Invalid as ex:
120 | for arg, error in ex.error_dict.items():
121 | errors.append("%s: %s ('%s')" % (arg,error.msg,error.value))
122 |
123 | # custom validators
124 | for arg in vKwargs.keys():
125 | validator = vKwargs[arg]
126 | if arg in kwargs:
127 | val = kwargs[arg]
128 | try:
129 | validator.to_python(val)
130 | except Invalid as ex:
131 | errors.append("%s: %s ('%s')" % (arg,ex,val))
132 | else:
133 | if isinstance(validator,FancyValidator) and validator.not_empty:
134 | raise TypeError("Missing mandatory argument '%s'" % arg)
135 |
136 | # fire error if anything failed validation
137 | if len(errors) > 0:
138 | raise TypeError('\n'.join(errors))
139 | # all OK
140 | return realfn(*args,**kwargs)
141 | return wrap
142 | return fn
143 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | """
2 | Twisted REST micro-framework
3 | ================================
4 |
5 | Based on *Flask* API, with plans for integrated multiprocessing support for full usage of all CPUs.
6 | Provides a more Flask/Sinatra-style API on top of the core *twisted.web* APIs.
7 | Integrates FormEncode for path, form and query argument validation.
8 |
9 | An example of a multi--module twisted.web CorePost REST application
10 | which exposes two separate REST services (for a Customer and Customer Address entities):
11 |
12 | ::
13 |
14 | class CustomerRESTService():
15 | path = "/customer"
16 |
17 | @route("/")
18 | def getAll(self,request):
19 | return DB.getAllCustomers()
20 |
21 | @route("/")
22 | def get(self,request,customerId):
23 | return DB.getCustomer(customerId)
24 |
25 | @route("/",Http.POST)
26 | def post(self,request,customerId,firstName,lastName):
27 | customer = Customer(customerId, firstName, lastName)
28 | DB.saveCustomer(customer)
29 | return Response(201)
30 |
31 | @route("/",Http.PUT)
32 | def put(self,request,customerId,firstName,lastName):
33 | c = DB.getCustomer(customerId)
34 | (c.firstName,c.lastName) = (firstName,lastName)
35 | return Response(200)
36 |
37 | @route("/",Http.DELETE)
38 | def delete(self,request,customerId):
39 | DB.deleteCustomer(customerId)
40 | return Response(200)
41 |
42 | @route("/",Http.DELETE)
43 | def deleteAll(self,request):
44 | DB.deleteAllCustomers()
45 | return Response(200)
46 |
47 | class CustomerAddressRESTService():
48 | path = "/customer//address"
49 |
50 | @route("/")
51 | def getAll(self,request,customerId):
52 | return DB.getCustomer(customerId).addresses
53 |
54 | @route("/")
55 | def get(self,request,customerId,addressId):
56 | return DB.getCustomerAddress(customerId, addressId)
57 |
58 | @route("/",Http.POST)
59 | def post(self,request,customerId,addressId,streetNumber,streetName,stateCode,countryCode):
60 | c = DB.getCustomer(customerId)
61 | address = CustomerAddress(streetNumber,streetName,stateCode,countryCode)
62 | c.addresses[addressId] = address
63 | return Response(201)
64 |
65 | @route("/",Http.PUT)
66 | def put(self,request,customerId,addressId,streetNumber,streetName,stateCode,countryCode):
67 | address = DB.getCustomerAddress(customerId, addressId)
68 | (address.streetNumber,address.streetName,address.stateCode,address.countryCode) = (streetNumber,streetName,stateCode,countryCode)
69 | return Response(200)
70 |
71 | @route("/",Http.DELETE)
72 | def delete(self,request,customerId,addressId):
73 | DB.getCustomerAddress(customerId, addressId) #validate address exists
74 | del(DB.getCustomer(customerId).addresses[addressId])
75 | return Response(200)
76 |
77 | @route("/",Http.DELETE)
78 | def deleteAll(self,request,customerId):
79 | c = DB.getCustomer(customerId)
80 | c.addresses = {}
81 | return Response(200)
82 |
83 |
84 | def run_rest_app():
85 | app = RESTResource((CustomerRESTService(),CustomerAddressRESTService()))
86 | app.run(8080)
87 |
88 | if __name__ == "__main__":
89 | run_rest_app()
90 |
91 | And the BDD showing off its different features
92 |
93 | https://github.com/jacek99/corepost/blob/master/corepost/test/feature/rest_app.feature
94 |
95 | Links
96 | `````
97 |
98 | * `Website `_
99 | * `Twisted `_
100 | * `FormEncode `_
101 |
102 | Changelog
103 | `````````
104 | * 0.0.16:
105 | - minor bug fix for issue #4 (serializing object graphs to XML), removed Jinja2 as dependency:
106 | https://github.com/jacek99/corepost/issues/4
107 | * 0.0.15:
108 | - minor bug fixes in auto-converting responses to JSON and parsing arguments/paths with unexpectec characters
109 | * 0.0.14:
110 | - automatic parsing of query, form, JSON, YAML and XML arguments:
111 | http://jacek99.github.com/corepost/argument_parsing.html
112 | * 0.0.13:
113 | - perf fix to avoid unnecessary string concatenation when doing URL routing, after code review (thanks to Gerald Tremblay)
114 | * 0.0.12:
115 | - backwards incompatible change: added advanced URL routing for nested REST services.
116 | CorePost object is gone, REST services are now just standard classes.
117 | They get wrapped in a RESTResource object (see sample above) when exposed
118 | * 0.0.11:
119 | - added support for request/response filters
120 | * 0.0.10:
121 | - removed dependency on txZMQ which was not needed at this point (yet)
122 | * 0.0.9:
123 | - fix for issue #3 (wrong class passes as 'self' to router method):
124 | https://github.com/jacek99/corepost/issues/3
125 | * 0.0.8:
126 | - support for serializing of classes to JSON,XML,YAML based on caller's Accept header
127 | - separate routing functionality from CorePost Resource object, in preparation for future multicore support
128 | * 0.0.7:
129 | - automatic parsing of incoming content (JSON, YAML, XML)
130 | - routing by incoming content type
131 | - automatic response conversion based on caller's Accept header (JSON/YAML
132 | - support for defer.returnValue() in @inlineCallbacks route methods
133 | * 0.0.6 - redesigned API around classes and methods, rather than functions and global objects (after feedback from Twisted devs)
134 | * 0.0.5 - added FormEncode validation for arguments
135 | * 0.0.4 - path argument extraction, mandatory argument error checking
136 |
137 | """
138 |
139 | from setuptools import setup
140 |
141 | setup(
142 | name="CorePost",
143 | version="0.0.16",
144 | author="Jacek Furmankiewicz",
145 | author_email="jacek99@gmail.com",
146 | description=("A Twisted Web REST micro-framework"),
147 | license="BSD",
148 | keywords="twisted rest flask sinatra get post put delete web",
149 | url="https://github.com/jacek99/corepost",
150 | packages=['corepost', ],
151 | long_description=__doc__,
152 | classifiers=[
153 | "Development Status :: 3 - Alpha",
154 | "Environment :: Web Environment",
155 | "Intended Audience :: Developers",
156 | "License :: OSI Approved :: BSD License",
157 | "Operating System :: OS Independent",
158 | "Programming Language :: Python",
159 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
160 | "Topic :: Software Development :: Libraries :: Python Modules",
161 | ],
162 | install_requires=[
163 | 'twisted>=12.0.0',
164 | 'formencode>=1.2.4',
165 | 'pyyaml>=3.1.0'
166 | ],
167 | tests_require=[
168 | 'httplib2>=0.7.1',
169 | 'freshen>=0.2',
170 | ],
171 | zip_safe = True
172 | )
173 |
--------------------------------------------------------------------------------
/test.sh:
--------------------------------------------------------------------------------
1 | nosetests --with-freshen -v
2 |
3 |
--------------------------------------------------------------------------------