Skip to content

Commit

Permalink
Merge branch 'feature/rpc' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
klen committed Oct 10, 2012
2 parents 1546018 + 192dd10 commit 8032b30
Show file tree
Hide file tree
Showing 15 changed files with 443 additions and 156 deletions.
36 changes: 30 additions & 6 deletions adrest/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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:
Expand All @@ -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)))
Expand All @@ -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)
7 changes: 7 additions & 0 deletions adrest/mixin/emitter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
122 changes: 102 additions & 20 deletions adrest/resources/rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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()))
Expand All @@ -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
57 changes: 43 additions & 14 deletions adrest/tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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')
Expand Down
22 changes: 15 additions & 7 deletions adrest/utils/emitter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -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)
Expand All @@ -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'

Expand Down Expand Up @@ -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>'

Expand Down Expand Up @@ -194,3 +200,5 @@ def serialize(content):

except ImportError:
pass

# pymode:lint_ignore=F0401,W0704
Loading

0 comments on commit 8032b30

Please sign in to comment.