~ubuntu-branches/ubuntu/saucy/python-wsme/saucy-proposed

« back to all changes in this revision

Viewing changes to wsmeext/extdirect/protocol.py

  • Committer: Package Import Robot
  • Author(s): Thomas Goirand
  • Date: 2013-04-27 13:06:20 UTC
  • mfrom: (0.1.3 experimental) (1.1.1)
  • Revision ID: package-import@ubuntu.com-20130427130620-v7mcfqp3wctxxcik
Tags: 0.5b2-1
New upstream release (Closes: #706238).

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
import datetime
 
2
import decimal
 
3
 
 
4
from simplegeneric import generic
 
5
 
 
6
from wsme.exc import ClientSideError
 
7
from wsme.protocol import CallContext, Protocol, expose
 
8
from wsme.utils import parse_isodate, parse_isodatetime, parse_isotime
 
9
from wsme.rest.args import from_params
 
10
from wsme.types import iscomplex, isusertype, list_attributes, Unset
 
11
import wsme.types
 
12
 
 
13
try:
 
14
    import simplejson as json
 
15
except ImportError:
 
16
    import json  # noqa
 
17
 
 
18
from six import u
 
19
 
 
20
 
 
21
class APIDefinitionGenerator(object):
 
22
    tpl = """\
 
23
Ext.ns("%(rootns)s");
 
24
 
 
25
if (!%(rootns)s.wsroot) {
 
26
    %(rootns)s.wsroot = "%(webpath)s.
 
27
}
 
28
 
 
29
%(descriptors)s
 
30
 
 
31
Ext.syncRequire(['Ext.direct.*'], function() {
 
32
  %(providers)s
 
33
});
 
34
"""
 
35
    descriptor_tpl = """\
 
36
Ext.ns("%(fullns)s");
 
37
 
 
38
%(fullns)s.Descriptor = {
 
39
    "url": %(rootns)s.wsroot + "extdirect/router/%(ns)s",
 
40
    "namespace": "%(fullns)s",
 
41
    "type": "remoting",
 
42
    "actions": %(actions)s
 
43
    "enableBuffer": true
 
44
};
 
45
"""
 
46
    provider_tpl = """\
 
47
    Ext.direct.Manager.addProvider(%(fullns)s.Descriptor);
 
48
"""
 
49
 
 
50
    def __init__(self):
 
51
        pass
 
52
 
 
53
    def render(self, rootns, webpath, namespaces, fullns):
 
54
        descriptors = u('')
 
55
        for ns in sorted(namespaces):
 
56
            descriptors += self.descriptor_tpl % {
 
57
                'ns': ns,
 
58
                'rootns': rootns,
 
59
                'fullns': fullns(ns),
 
60
                'actions': '\n'.join((
 
61
                    ' ' * 4 + line
 
62
                    for line
 
63
                    in json.dumps(namespaces[ns], indent=4).split('\n')
 
64
                ))
 
65
            }
 
66
 
 
67
        providers = u('')
 
68
        for ns in sorted(namespaces):
 
69
            providers += self.provider_tpl % {
 
70
                'fullns': fullns(ns)
 
71
            }
 
72
 
 
73
        r = self.tpl % {
 
74
            'rootns': rootns,
 
75
            'webpath': webpath,
 
76
            'descriptors': descriptors,
 
77
            'providers': providers,
 
78
        }
 
79
        return r
 
80
 
 
81
 
 
82
@generic
 
83
def fromjson(datatype, value):
 
84
    if iscomplex(datatype):
 
85
        newvalue = datatype()
 
86
        for attrdef in list_attributes(datatype):
 
87
            if attrdef.name in value:
 
88
                setattr(newvalue, attrdef.key,
 
89
                        fromjson(attrdef.datatype, value[attrdef.name]))
 
90
        value = newvalue
 
91
    elif isusertype(datatype):
 
92
        value = datatype.frombasetype(fromjson(datatype.basetype, value))
 
93
    return value
 
94
 
 
95
 
 
96
@generic
 
97
def tojson(datatype, value):
 
98
    if value is None:
 
99
        return value
 
100
    if iscomplex(datatype):
 
101
        d = {}
 
102
        for attrdef in list_attributes(datatype):
 
103
            attrvalue = getattr(value, attrdef.key)
 
104
            if attrvalue is not Unset:
 
105
                d[attrdef.name] = tojson(attrdef.datatype, attrvalue)
 
106
        value = d
 
107
    elif isusertype(datatype):
 
108
        value = tojson(datatype.basetype, datatype.tobasetype(value))
 
109
    return value
 
110
 
 
111
 
 
112
@fromjson.when_type(wsme.types.ArrayType)
 
113
def array_fromjson(datatype, value):
 
114
    return [fromjson(datatype.item_type, item) for item in value]
 
115
 
 
116
 
 
117
@tojson.when_type(wsme.types.ArrayType)
 
118
def array_tojson(datatype, value):
 
119
    if value is None:
 
120
        return value
 
121
    return [tojson(datatype.item_type, item) for item in value]
 
122
 
 
123
 
 
124
@fromjson.when_type(wsme.types.DictType)
 
125
def dict_fromjson(datatype, value):
 
126
    if value is None:
 
127
        return value
 
128
    return dict((
 
129
        (fromjson(datatype.key_type, key),
 
130
            fromjson(datatype.value_type, value))
 
131
        for key, value in value.items()
 
132
    ))
 
133
 
 
134
 
 
135
@tojson.when_type(wsme.types.DictType)
 
136
def dict_tojson(datatype, value):
 
137
    if value is None:
 
138
        return value
 
139
    return dict((
 
140
        (tojson(datatype.key_type, key),
 
141
            tojson(datatype.value_type, value))
 
142
        for key, value in value.items()
 
143
    ))
 
144
 
 
145
 
 
146
@tojson.when_object(wsme.types.bytes)
 
147
def bytes_tojson(datatype, value):
 
148
    if value is None:
 
149
        return value
 
150
    return value.decode('ascii')
 
151
 
 
152
 
 
153
# raw strings
 
154
@fromjson.when_object(wsme.types.bytes)
 
155
def bytes_fromjson(datatype, value):
 
156
    if value is not None:
 
157
        value = value.encode('ascii')
 
158
    return value
 
159
 
 
160
 
 
161
# unicode strings
 
162
 
 
163
@fromjson.when_object(wsme.types.text)
 
164
def text_fromjson(datatype, value):
 
165
    if isinstance(value, wsme.types.bytes):
 
166
        return value.decode('utf-8')
 
167
    return value
 
168
 
 
169
 
 
170
# datetime.time
 
171
 
 
172
@fromjson.when_object(datetime.time)
 
173
def time_fromjson(datatype, value):
 
174
    if value is None or value == '':
 
175
        return None
 
176
    return parse_isotime(value)
 
177
 
 
178
 
 
179
@tojson.when_object(datetime.time)
 
180
def time_tojson(datatype, value):
 
181
    if value is None:
 
182
        return value
 
183
    return value.isoformat()
 
184
 
 
185
 
 
186
# datetime.date
 
187
 
 
188
@fromjson.when_object(datetime.date)
 
189
def date_fromjson(datatype, value):
 
190
    if value is None or value == '':
 
191
        return None
 
192
    return parse_isodate(value)
 
193
 
 
194
 
 
195
@tojson.when_object(datetime.date)
 
196
def date_tojson(datatype, value):
 
197
    if value is None:
 
198
        return value
 
199
    return value.isoformat()
 
200
 
 
201
 
 
202
# datetime.datetime
 
203
 
 
204
@fromjson.when_object(datetime.datetime)
 
205
def datetime_fromjson(datatype, value):
 
206
    if value is None or value == '':
 
207
        return None
 
208
    return parse_isodatetime(value)
 
209
 
 
210
 
 
211
@tojson.when_object(datetime.datetime)
 
212
def datetime_tojson(datatype, value):
 
213
    if value is None:
 
214
        return value
 
215
    return value.isoformat()
 
216
 
 
217
 
 
218
# decimal.Decimal
 
219
 
 
220
@fromjson.when_object(decimal.Decimal)
 
221
def decimal_fromjson(datatype, value):
 
222
    if value is None:
 
223
        return value
 
224
    return decimal.Decimal(value)
 
225
 
 
226
 
 
227
@tojson.when_object(decimal.Decimal)
 
228
def decimal_tojson(datatype, value):
 
229
    if value is None:
 
230
        return value
 
231
    return str(value)
 
232
 
 
233
 
 
234
class ExtCallContext(CallContext):
 
235
    def __init__(self, request, namespace, calldata):
 
236
        super(ExtCallContext, self).__init__(request)
 
237
        self.namespace = namespace
 
238
 
 
239
        self.tid = calldata['tid']
 
240
        self.action = calldata['action']
 
241
        self.method = calldata['method']
 
242
        self.params = calldata['data']
 
243
 
 
244
 
 
245
class FormExtCallContext(CallContext):
 
246
    def __init__(self, request, namespace):
 
247
        super(FormExtCallContext, self).__init__(request)
 
248
        self.namespace = namespace
 
249
 
 
250
        self.tid = request.params['extTID']
 
251
        self.action = request.params['extAction']
 
252
        self.method = request.params['extMethod']
 
253
        self.params = []
 
254
 
 
255
 
 
256
class ExtDirectProtocol(Protocol):
 
257
    """
 
258
    ExtDirect protocol.
 
259
 
 
260
    For more detail on the protocol, see
 
261
    http://www.sencha.com/products/extjs/extdirect.
 
262
 
 
263
    .. autoattribute:: name
 
264
    .. autoattribute:: content_types
 
265
    """
 
266
    name = 'extdirect'
 
267
    displayname = 'ExtDirect'
 
268
    content_types = ['application/json', 'text/javascript']
 
269
 
 
270
    def __init__(self, namespace='', params_notation='named',
 
271
            nsfolder=None):
 
272
        self.namespace = namespace
 
273
        self.appns, self.apins = namespace.rsplit('.', 2) \
 
274
            if '.' in namespace else (namespace, '')
 
275
        self.default_params_notation = params_notation
 
276
        self.appnsfolder = nsfolder
 
277
 
 
278
    @property
 
279
    def api_alias(self):
 
280
        if self.appnsfolder:
 
281
            alias = '/%s/%s.js' % (
 
282
                self.appnsfolder,
 
283
                self.apins.replace('.', '/'))
 
284
            return alias
 
285
 
 
286
    def accept(self, req):
 
287
        path = req.path
 
288
        assert path.startswith(self.root._webpath)
 
289
        path = path[len(self.root._webpath):]
 
290
 
 
291
        return path == self.api_alias or \
 
292
                     path == "/extdirect/api" or \
 
293
                     path.startswith("/extdirect/router")
 
294
 
 
295
    def iter_calls(self, req):
 
296
        path = req.path
 
297
 
 
298
        assert path.startswith(self.root._webpath)
 
299
        path = path[len(self.root._webpath):].strip()
 
300
 
 
301
        assert path.startswith('/extdirect/router'), path
 
302
        path = path[17:].strip('/')
 
303
 
 
304
        if path:
 
305
            namespace = path.split('.')
 
306
        else:
 
307
            namespace = []
 
308
 
 
309
        if 'extType' in req.params:
 
310
            req.wsme_extdirect_batchcall = False
 
311
            yield FormExtCallContext(req, namespace)
 
312
        else:
 
313
            data = json.loads(req.body.decode('utf8'))
 
314
            req.wsme_extdirect_batchcall = isinstance(data, list)
 
315
            if not req.wsme_extdirect_batchcall:
 
316
                data = [data]
 
317
            req.callcount = len(data)
 
318
 
 
319
            for call in data:
 
320
                yield ExtCallContext(req, namespace, call)
 
321
 
 
322
    def extract_path(self, context):
 
323
        path = list(context.namespace)
 
324
 
 
325
        if context.action:
 
326
            path.append(context.action)
 
327
 
 
328
        path.append(context.method)
 
329
 
 
330
        return path
 
331
 
 
332
    def read_std_arguments(self, context):
 
333
        funcdef = context.funcdef
 
334
        notation = funcdef.extra_options.get(
 
335
                'extdirect_params_notation', self.default_params_notation)
 
336
        args = context.params
 
337
        if notation == 'positional':
 
338
            kw = dict((argdef.name, fromjson(argdef.datatype, arg))
 
339
                    for argdef, arg in zip(funcdef.arguments, args))
 
340
        elif notation == 'named':
 
341
            if len(args) == 0:
 
342
                args = [{}]
 
343
            elif len(args) > 1:
 
344
                raise ClientSideError(
 
345
                    "Named arguments: takes a single object argument")
 
346
            args = args[0]
 
347
            kw = dict(
 
348
                (argdef.name, fromjson(argdef.datatype, args[argdef.name]))
 
349
                for argdef in funcdef.arguments if argdef.name in args
 
350
            )
 
351
        else:
 
352
            raise ValueError("Invalid notation: %s" % notation)
 
353
        return kw
 
354
 
 
355
    def read_form_arguments(self, context):
 
356
        kw = {}
 
357
        for argdef in context.funcdef.arguments:
 
358
            value = from_params(argdef.datatype,
 
359
                context.request.params, argdef.name, set())
 
360
            if value is not Unset:
 
361
                kw[argdef.name] = value
 
362
        return kw
 
363
 
 
364
    def read_arguments(self, context):
 
365
        if isinstance(context, ExtCallContext):
 
366
            kwargs = self.read_std_arguments(context)
 
367
        elif isinstance(context, FormExtCallContext):
 
368
            kwargs = self.read_form_arguments(context)
 
369
        wsme.runtime.check_arguments(context.funcdef, (), kwargs)
 
370
        return kwargs
 
371
 
 
372
    def encode_result(self, context, result):
 
373
        return json.dumps({
 
374
            'type': 'rpc',
 
375
            'tid': context.tid,
 
376
            'action': context.action,
 
377
            'method': context.method,
 
378
            'result': tojson(context.funcdef.return_type, result)
 
379
        })
 
380
 
 
381
    def encode_error(self, context, infos):
 
382
        return json.dumps({
 
383
            'type': 'exception',
 
384
            'tid': context.tid,
 
385
            'action': context.action,
 
386
            'method': context.method,
 
387
            'message': '%(faultcode)s: %(faultstring)s' % infos,
 
388
            'where': infos['debuginfo']})
 
389
 
 
390
    def prepare_response_body(self, request, results):
 
391
        r = ",\n".join(results)
 
392
        if request.wsme_extdirect_batchcall:
 
393
            return "[\n%s\n]" % r
 
394
        else:
 
395
            return r
 
396
 
 
397
    def get_response_status(self, request):
 
398
        return 200
 
399
 
 
400
    def get_response_contenttype(self, request):
 
401
        return "text/javascript"
 
402
 
 
403
    def fullns(self, ns):
 
404
        return ns and '%s.%s' % (self.namespace, ns) or self.namespace
 
405
 
 
406
    @expose('/extdirect/api', "text/javascript")
 
407
    @expose('${api_alias}', "text/javascript")
 
408
    def api(self):
 
409
        namespaces = {}
 
410
        for path, funcdef in self.root.getapi():
 
411
            if len(path) > 1:
 
412
                namespace = '.'.join(path[:-2])
 
413
                action = path[-2]
 
414
            else:
 
415
                namespace = ''
 
416
                action = ''
 
417
            if namespace not in namespaces:
 
418
                namespaces[namespace] = {}
 
419
            if action not in namespaces[namespace]:
 
420
                namespaces[namespace][action] = []
 
421
            notation = funcdef.extra_options.get('extdirect_params_notation',
 
422
                self.default_params_notation)
 
423
            method = {
 
424
                'name': funcdef.name}
 
425
 
 
426
            if funcdef.extra_options.get('extdirect_formhandler', False):
 
427
                method['formHandler'] = True
 
428
            method['len'] = 1 if notation == 'named' \
 
429
                            else len(funcdef.arguments)
 
430
            namespaces[namespace][action].append(method)
 
431
        webpath = self.root._webpath
 
432
        if webpath and not webpath.endswith('/'):
 
433
            webpath += '/'
 
434
        return APIDefinitionGenerator().render(
 
435
            namespaces=namespaces,
 
436
            webpath=webpath,
 
437
            rootns=self.namespace,
 
438
            fullns=self.fullns,
 
439
        )
 
440
 
 
441
    def encode_sample_value(self, datatype, value, format=False):
 
442
        r = tojson(datatype, value)
 
443
        content = json.dumps(r, ensure_ascii=False,
 
444
            indent=4 if format else 0,
 
445
            sort_keys=format)
 
446
        return ('javascript', content)