Permalink
Browse files

Merge branch 'feature/rpc' into develop

  • Loading branch information...
2 parents 1546018 + 192dd10 commit 8032b305d8e3588330d870c0e714457bcbb2ae7b @klen committed Oct 10, 2012
View
@@ -2,10 +2,12 @@
from django.conf.urls.defaults import patterns
from django.dispatch import Signal
+from django.http import HttpRequest
from .resources.map import MapResource
-from .resources.rpc import RPCResource
+from .resources.rpc import AutoJSONRPC
from .views import ResourceView
+from .utils import exceptions, status, tools, emitter
LOG = logging.getLogger('adrest')
@@ -37,8 +39,12 @@ def __init__(self, version=None, api_map=True, api_prefix='api', api_rpc=False,
if api_map:
self.resources[MapResource.meta.url_name] = MapResource
+ # Enable Auto JSON RPC resource
if api_rpc:
- self.resources[RPCResource.meta.url_name] = RPCResource
+ self.resources[AutoJSONRPC.meta.url_name] = AutoJSONRPC
+ self.params['emitters'] = tools.as_tuple(params.get('emitters', [])) + (
+ emitter.JSONPEmitter, emitter.JSONEmitter
+ )
if not isinstance(self.str_version, basestring):
try:
@@ -63,10 +69,16 @@ def register(self, resource, **params):
# Fabric of resources
params = dict(self.params, **params)
if params:
- params['name'] = ''.join(bit for bit in resource.__name__.split('Resource') if bit).lower()
- params['__module__'] = '%s.%s' % (self.prefix, self.str_version.replace('.', '_'))
+ params['name'] = ''.join(bit for bit in resource.__name__.split(
+ 'Resource') if bit).lower()
+
+ params['__module__'] = '%s.%s' % (
+ self.prefix, self.str_version.replace('.', '_'))
+
params['__doc__'] = resource.__doc__
- resource = type('%s%s' % (resource.__name__, len(self.resources)), (resource,), params)
+
+ resource = type('%s%s' % (
+ resource.__name__, len(self.resources)), (resource,), params)
if self.resources.get(resource.meta.url_name):
LOG.warning("A new resource '%r' is replacing the existing record for '%s'" % (resource, self.resources.get(resource.url_name)))
@@ -85,8 +97,20 @@ def urls(self):
resource = self.resources[url_name]
urls.append(resource.as_url(
api=self,
- name_prefix='-'.join((self.prefix, self.str_version)).strip('-'),
+ name_prefix='-'.join(
+ (self.prefix, self.str_version)).strip('-'),
url_prefix=self.str_version
))
return patterns(self.prefix, *urls)
+
+ def call(self, name, request=None, **params):
+ """ Call resource by ``Api`` name.
+ """
+ if not name in self.resources:
+ raise exceptions.HttpError('Unknown method \'%s\'' % name,
+ status=status.HTTP_501_NOT_IMPLEMENTED)
+ request = request or HttpRequest()
+ resource = self.resources[name]
+ view = resource.as_view(api=self)
+ return view(request, **params)
View
@@ -14,9 +14,16 @@ def __new__(mcs, name, bases, params):
params['meta'] = params.get('meta', MetaOptions())
cls = super(EmitterMeta, mcs).__new__(mcs, name, bases, params)
cls.emitters = as_tuple(cls.emitters)
+ cache = set()
cls.meta.default_emitter = cls.emitters[0] if cls.emitters else None
for e in cls.emitters:
assert issubclass(e, BaseEmitter), "Emitter must be subclass of BaseEmitter"
+
+ # Skip dublicates
+ if e in cache:
+ continue
+ cache.add(e)
+
cls.meta.emitters_dict[e.media_type] = e
cls.meta.emitters_types.append(e.media_type)
return cls
View
@@ -2,22 +2,27 @@
from django.utils import simplejson
from ..utils.emitter import JSONPEmitter, JSONEmitter
+from ..utils.parser import JSONParser, FormParser
from ..utils.exceptions import HttpError
-from ..utils.status import HTTP_402_PAYMENT_REQUIRED
from ..views import ResourceView
+from ..utils.status import HTTP_409_CONFLICT
+from ..utils.tools import as_tuple
-class RPCResource(ResourceView):
- " Auto generated RPC. "
+class JSONRPCResource(ResourceView):
+ """
+ JSON RPC support.
+ -----------------
+
+ Implementation of remote procedure call encoded in JSON.
+ Allows for notifications (info sent to the server that does not require a response)
+ and for multiple calls to be sent to the server which may be answered out of order.
+ """
url_regex = r'^rpc$'
emitters = JSONEmitter, JSONPEmitter
separator = '.'
- def __init__(self, *args, **kwargs):
- self.target_resource = None
- super(RPCResource, self).__init__(*args, **kwargs)
-
def get(self, request, **resources):
try:
payload = request.GET.get('payload')
@@ -27,9 +32,7 @@ def get(self, request, **resources):
method = payload['method']
assert method and self.separator in method, "Wrong method name: %s." % method
- resource, method = method.split(self.separator, 1)
- resource = self.api.resources.get(resource)
- assert resource and hasattr(resource, method), "Wrong resource: %s.%s" % (resource, method)
+ resource_name, method = method.split(self.separator, 1)
data = QueryDict('', mutable=True)
data.update(payload.get('data', dict()))
@@ -44,19 +47,98 @@ def get(self, request, **resources):
request.method = method.upper()
except AssertionError, e:
- raise HttpError('Invalid RPC Call. %s' % e, status=HTTP_402_PAYMENT_REQUIRED)
+ raise HttpError('Invalid RPC Call. %s' % e, status=HTTP_409_CONFLICT)
except (ValueError, KeyError, TypeError):
- raise HttpError('Invalid RPC Payload.', status=HTTP_402_PAYMENT_REQUIRED)
+ raise HttpError('Invalid RPC Payload.', status=HTTP_409_CONFLICT)
+
+ params = payload.get('params', dict())
+ response = self.api.call(resource_name, request, **params)
+ response.finaly = True
+ return response
+
+
+class RPCResource(ResourceView):
+
+ allowed_methods = 'get', 'post'
+ url_regex = r'^rpc$'
+ emitters = JSONEmitter, JSONPEmitter
+ parsers = JSONParser, FormParser
+ scheme = None
+
+ def __init__(self, scheme=None, **kwargs):
+ self.methods = dict()
+ if scheme:
+ self.scheme = scheme
+ self.configure_rpc(self.scheme)
+ super(RPCResource, self).__init__(**kwargs)
+
+ def configure_rpc(self, scheme):
+ if scheme is None:
+ raise ValueError("Invalid RPC scheme.")
+
+ for m in [getattr(scheme, m) for m in dir(scheme) if hasattr(getattr(scheme, m), '__call__')]:
+ self.methods[m.__name__] = m
+
+ def handle_request(self, request, **resources):
+ payload = request.data
+
+ try:
+
+ if request.method == 'GET':
+ payload = request.GET.get('payload')
+ try:
+ payload = simplejson.loads(payload)
+ except TypeError:
+ raise AssertionError("Invalid RPC Call.")
+
+ assert 'method' in payload, "Invalid RPC Call."
+ return self.rpc_call(request, **payload)
+
+ except Exception, e:
+ return dict(error=dict(message=str(e)))
+
+ def rpc_call(self, request, method=None, params=None, **kwargs):
+ args = []
+ kwargs = dict()
+ if isinstance(params, dict):
+ kwargs.update(params)
+ else:
+ args = as_tuple(params)
+
+ assert method in self.methods, "Unknown method: {0}".format(method)
+ return self.methods[method](*args, **kwargs)
+
+
+class AutoJSONRPC(RPCResource):
+ separator = '.'
+
+ def configure_rpc(self, scheme):
+ pass
+
+ def rpc_call(self, request, method=None, **payload):
+ """ Call REST API with RPC force.
+ """
+ assert method and self.separator in method, "Wrong method name: {0}".format(method)
+
+ resource_name, method = method.split(self.separator, 1)
+ assert resource_name in self.api.resources, "Unknown method"
- self.target_resource = resource
- resource = resource.as_view(api=self.api)
- return resource(request, _emit_=False, **payload.get("params", dict()))
+ data = QueryDict('', mutable=True)
+ data.update(payload.get('data', dict()))
+ data['callback'] = payload.get('callback') or request.GET.get('callback') or request.GET.get('jsonp') or 'callback'
+ for h, v in payload.get('headers', dict()).iteritems():
+ request.META["HTTP_%s" % h.upper().replace('-', '_')] = v
- def get_name(self):
- if self.target_resource:
- return self.target_resource.meta.name
- return self.meta.name
+ request.POST = request.PUT = request.GET = data
+ delattr(request, '_request')
+ request.method = method.upper()
+ request.META['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'
+ params = payload.pop('params', dict())
+ response = self.api.call(resource_name, request, **params)
+ response.finaly = True
+ assert response.status_code == 200, response.content
+ return response
-# pymode:lint_ignore=E1103
+# pymode:lint_ignore=E1103,W0703
View
@@ -38,10 +38,15 @@ class AdrestTestCase(TestCase):
api = None
def setUp(self):
- assert self.api, "API must be defined"
+ assert self.api, "AdrestTestCase must have the api attribute."
self.client = AdrestClient()
def reverse(self, resource, **kwargs):
+ """ Reverse resource by ResourceClass or name.
+
+ :param resource: Resource Class or String name.
+ :param **kwargs: Uri params
+ """
if isinstance(resource, basestring):
url_name = resource
assert self.api.resources.get(url_name), "Invalid resource name: %s" % url_name
@@ -53,26 +58,50 @@ def reverse(self, resource, **kwargs):
name_ver = '' if not str(self.api) else '%s-' % str(self.api)
return reverse('%s-%s%s' % (self.api.prefix, name_ver, url_name), kwargs=kwargs)
- def get_resource(self, resource, method='get', data=None, key=None, headers=None, **kwargs):
- uri = self.reverse(resource, **kwargs)
- method = getattr(self.client, method)
+ def get_params(self, resource, headers=None, data=None, key=None, **kwargs):
+ headers = headers or dict()
+ data = data or dict()
if isinstance(key, Model):
key = key.key
- headers = dict() if headers is None else headers
headers['HTTP_AUTHORIZATION'] = key or headers.get('HTTP_AUTHORIZATION')
- return method(uri, data=data or dict(), **headers)
+ resource = self.reverse(resource, **kwargs)
+ return resource, headers, data
+
+ def get_resource(self, resource, method='get', data=None, headers=None, **kwargs):
+ """ Simply run resource method.
+
+ :param resource: Resource Class or String name.
+ :param data: Request data
+ :param headers: Request headers
+ :param key: HTTP_AUTHORIZATION token
+ """
+ method = getattr(self.client, method)
+ resource, headers, data = self.get_params(resource, headers, data, **kwargs)
+ return method(resource, data=data, **headers)
+
+ def rpc(self, resource, rpc=None, headers=None, callback=None, **kwargs):
+ """ Emulate RPC call.
+
+ :param resource: Resource Class or String name.
+ :param rpc: RPC params.
+ :param headers: Send headers
+ :param callback: JSONP callback
+ """
+ resource, headers, data = self.get_params(resource, headers, data=rpc, **kwargs)
- def rpc(self, data, callback=None, headers=None, key=None, **kwargs):
- data = dict(payload=simplejson.dumps(data))
if callback:
- data['callback'] = callback
+ headers['HTTP_ACCEPT'] = 'text/javascript'
+ method = self.client.get
+ data = dict(
+ callback=callback,
+ payload=simplejson.dumps(data))
- # JSONP not support headers
- if headers and headers.get('HTTP_ACCEPT') == 'text/javascript':
- headers = dict(HTTP_ACCEPT='text/javascript')
- key = None
+ else:
+ headers['HTTP_ACCEPT'] = 'application/json'
+ method = self.client.post
+ data = simplejson.dumps(data)
- return self.get_resource('rpc', data=data, headers=headers, key=key, **kwargs)
+ return method(resource, data=data, content_type='application/json', **headers)
put_resource = curry(get_resource, method='put')
post_resource = curry(get_resource, method='post')
View
@@ -3,7 +3,6 @@
from time import mktime
from django.db.models.base import ModelBase, Model
-from django.http import HttpResponse
from django.template import RequestContext, loader
from ..utils import UpdatedList
@@ -15,6 +14,7 @@
class EmitterMeta(type):
" Preload format attribute. "
+
def __new__(mcs, name, bases, params):
cls = super(EmitterMeta, mcs).__new__(mcs, name, bases, params)
if not cls.format and cls.media_type:
@@ -34,12 +34,13 @@ class BaseEmitter(object):
def __init__(self, resource, request=None, response=None):
self.resource = resource
self.request = request
- self.response = response
- if not isinstance(response, HttpResponse):
- self.response = SerializedHttpResponse(response, mimetype=self.media_type, status=HTTP_200_OK)
+ self.response = SerializedHttpResponse(
+ response,
+ mimetype=self.media_type,
+ status=HTTP_200_OK)
def emit(self):
- if not isinstance(self.response, SerializedHttpResponse):
+ if self.response.finaly:
return self.response
self.response.content = self.serialize(self.response.response)
@@ -52,6 +53,13 @@ def serialize(content):
return content
+class NullEmitter(BaseEmitter):
+ media_type = 'unknown/unknown'
+
+ def emit(self):
+ return self.response
+
+
class TextEmitter(BaseEmitter):
media_type = 'text/plain'
@@ -79,13 +87,11 @@ class JSONPEmitter(JSONEmitter):
def serialize(self, content):
content = super(JSONPEmitter, self).serialize(content)
-
callback = self.request.GET.get('callback', 'callback')
return u'%s(%s)' % (callback, content)
class XMLEmitter(BaseEmitter):
-
media_type = 'application/xml'
xmldoc_tpl = '<?xml version="1.0" encoding="utf-8"?>\n<response success="%s" version="%s" timestamp="%s">%s</response>'
@@ -194,3 +200,5 @@ def serialize(content):
except ImportError:
pass
+
+# pymode:lint_ignore=F0401,W0704
Oops, something went wrong.

0 comments on commit 8032b30

Please sign in to comment.