# HG changeset patch # User Sandro Knauß # Date 1348748741 -7200 # Node ID 93686b0c028bbfc995ad1e598536faf04a20c073 # Parent a73bbc1d8b4bcc962d005d105d6d2998679debdf adding json interface diff -r a73bbc1d8b4b -r 93686b0c028b iro/iro.py --- a/iro/iro.py Thu Sep 27 14:20:03 2012 +0200 +++ b/iro/iro.py Thu Sep 27 14:25:41 2012 +0200 @@ -28,7 +28,7 @@ from sqlalchemy import create_engine, pool import config, install -from .view import xmlrpc, jsonrpc +from .view import xmlrpc, jsonrpc, jsonresource from .model import setEngine, setPool from .controller.pool import startPool, dbPool @@ -69,10 +69,12 @@ root = resource.Resource() xmlrpc.appendResource(root) jsonrpc.appendResource(root) + jsonresource.appendResource(root) v2 = resource.Resource() xmlrpc.appendResource(v2) jsonrpc.appendResource(v2) + jsonresource.appendResource(v2) root.putChild('1.0a', v2) internet.TCPServer(config.main.port, server.Site(root)).setServiceParent(top_service) diff -r a73bbc1d8b4b -r 93686b0c028b iro/main.py --- a/iro/main.py Thu Sep 27 14:20:03 2012 +0200 +++ b/iro/main.py Thu Sep 27 14:25:41 2012 +0200 @@ -27,7 +27,7 @@ from .model import setEngine, setPool from .controller.pool import startPool, dbPool -from .view import xmlrpc, jsonrpc +from .view import xmlrpc, jsonrpc, jsonresource from . import config def runReactor(reactor, engine, port, root): @@ -58,10 +58,12 @@ root = resource.Resource() xmlrpc.appendResource(root) jsonrpc.appendResource(root) + jsonresource.appendResource(root) v2 = resource.Resource() xmlrpc.appendResource(v2) jsonrpc.pappendResource(v2) + jsonresource.pappendResource(v2) root.putChild('1.0a', v2) runReactor(reactor, engine, config.main.port, root) diff -r a73bbc1d8b4b -r 93686b0c028b iro/tests/jsonresource.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/iro/tests/jsonresource.py Thu Sep 27 14:25:41 2012 +0200 @@ -0,0 +1,354 @@ +# Copyright (c) 2012 netzguerilla.net +# +# This file is part of Iro. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +# #Software, and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +# INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +# PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +from twisted.trial import unittest + +from twisted.web.client import Agent, ResponseDone +from twisted.internet import reactor +from twisted.web.http_headers import Headers +from twisted.python.failure import Failure +from twisted.internet.protocol import Protocol +from twisted.internet import defer + +from zope.interface import implements + +from twisted.internet.defer import succeed +from twisted.web.iweb import IBodyProducer + +import json + +from twisted.web import server +from txjsonrpc.web.jsonrpc import Fault + +from datetime import datetime + +from iro.model.schema import User, Offer, Userright, Job, Message + +from iro.view import jsonresource +import iro.error as IroError + +from iro.test_helpers.dbtestcase import DBTestCase + +from iro.model import setEngine, setPool +from iro.controller.pool import startPool, dbPool + + +class StringProducer(object): + implements(IBodyProducer) + + def __init__(self, body): + self.body = json.dumps(body) + self.length = len(self.body) + + def startProducing(self, consumer): + consumer.write(self.body) + return succeed(None) + + def pauseProducing(self): + pass + + def stopProducing(self): + pass + +class SimpleReceiver(Protocol): + def __init__(self, d): + self.d = d + self.data = "" + + def dataReceived(self, data): + self.data += data + + def connectionLost(self, reason): + try: + if reason.check(ResponseDone): + self.d.callback(json.loads(self.data)) + else: + self.d.errback(reason) + except: + self.d.errback(Failure()) + +class JSONRPCTest(DBTestCase): + """tests for the jsonrpc interface""" + ContentType = 'application/json' + StringProducer = StringProducer + def setUp(self): + DBTestCase.setUp(self) + + setEngine(self.engine) + startPool(reactor) + setPool(dbPool) + self.p = reactor.listenTCP(0, server.Site(jsonresource.JSONFactory()), + interface="127.0.0.1") + self.port = self.p.getHost().port + self.agent = Agent(reactor) + + def tearDown(self): + DBTestCase.tearDown(self) + return self.p.stopListening() + + def proxy(self, method, data=None, **kargs): + d = self.agent.request( + 'GET', + 'http://localhost:%d/%s'%(self.port, method), + Headers({'Content-Type':[self.ContentType]}), + self.StringProducer(data) if data else None, + ) + + def cbResponse(response): + if kargs.has_key('code'): + self.failUnlessEqual(response.code, kargs["code"]) + if response.code not in [200,400,500]: + raise Fault(response.code,'') + self.failUnlessEqual(response.headers.getRawHeaders('Content-Type'), ['application/json']) + d = defer.Deferred() + response.deliverBody(SimpleReceiver(d)) + def _(data): + if data["status"]: + return data["result"] + else: + raise Fault(data["error"]["code"],data["error"]["msg"]) + + d.addCallback(_) + return d + d.addCallback(cbResponse) + return d + + + def testListMethods(self): + '''list of all offical Methods, that can be executed''' + + def cbMethods(ret): + ret.sort() + self.failUnlessEqual(ret, ['bill', 'defaultRoute', 'email', 'fax', 'listMethods', 'mail', 'routes', 'sms', 'status', 'telnumber']) + + d=self.proxy("listMethods", code=200) + d.addCallback(cbMethods) + return d + + def testApikey(self): + ''' test apikey''' + with self.session() as session: + session.add(User(name='test',apikey='abcdef123456789')) + + d = self.proxy('status', ['abcdef123456789'], code=200) + d.addCallback(lambda ret: self.failUnlessEqual(ret,{})) + + return d + + def testStatus(self): + ''' test the status function''' + with self.session() as session: + u = User(name='test',apikey='abcdef123456789') + session.add(u) + j = Job(info='info', status="started") + j.user=u + session.add(j) + session.commit() + jid=j.id + status = {str(jid):{"status":"started"}} + args=[('abcdef123456789',jid), + ('abcdef123456789',), + ('abcdef123456789', '', 'false'), + ('abcdef123456789', '', 0), + ('abcdef123456789', '', 0), + {"id":jid, "user":'abcdef123456789'}, + ] + dl=[] + for a in args: + d = self.proxy('status', a, code=200) + d.addCallback(lambda ret: self.failUnlessEqual(ret,status)) + dl.append(d) + + def f(exc): + jnf = IroError.JobNotFound() + self.failUnlessEqual(exc.faultCode, jnf.code) + self.failUnlessEqual(exc.faultString, jnf.msg) + d = self.proxy('status', ['abcdef123456789', jid+1], code=500) + d = self.assertFailure(d, Fault) + d.addCallback(f) + dl.append(d) + + return defer.DeferredList(dl, fireOnOneErrback=True) + + def testNoSuchUser(self): + '''a unknown user should raise a UserNotNound Exception + bewcause xmlrpc only has a Fault exception this Exception has to be deliverd through a xmlrpclib.Fault Exception''' + def f(exc): + unf = IroError.UserNotFound() + self.failUnlessEqual(exc.faultCode, unf.code) + self.failUnlessEqual(exc.faultString, unf.msg) + d = self.proxy('status', ['abcdef123456789'], code=500) + d = self.assertFailure(d, Fault) + d.addCallback(f) + return d + + def testNoSuchMethod(self): + '''a unknown mothod should raise a Exception ''' + def f(exc): + self.failUnlessEqual(exc.faultCode, 404) + self.failUnlessEqual(exc.faultString, '') + d = self.proxy('nosuchmethod', code=404) + d = self.assertFailure(d, Fault) + d.addCallback(f) + return d + + def testValidationFault(self): + '''a validate Exception should be translated to a xmlrpclib.Fault.''' + def f(exc): + self.failUnlessEqual(exc.faultCode, 700) + self.failUnlessEqual(exc.faultString, "Validation of 'apikey' failed.") + d = self.proxy('status',{'user':'xxx'}, code=400) + d = self.assertFailure(d, Fault) + d.addCallback(f) + return d + + @defer.inlineCallbacks + def testRoutes(self): + '''test the route function''' + with self.session() as session: + u=User(name='test',apikey='abcdef123456789') + o=Offer(name="sipgate_basic", provider="sipgate", route="basic", typ="sms") + u.rights.append(Userright(o)) + session.add(u) + + x = yield self.proxy('routes',['abcdef123456789','sms'], code=200) + self.failUnlessEqual(x,['sipgate_basic']) + + def f(exc): + self.failUnlessEqual(exc.faultCode, 700) + self.failUnlessEqual(exc.faultString, "Typ fax is not valid.") + d = self.proxy('routes',['abcdef123456789','fax'], code=400) + d = self.assertFailure(d, Fault) + d.addCallback(f) + yield d + + with self.session() as session: + o=Offer(name="sipgate_plus", provider="sipgate", route="plus", typ="sms") + u = session.query(User).filter_by(name="test").first() + u.rights.append(Userright(o)) + o=Offer(name="faxde", provider="faxde", route="", typ="fax") + session.add(o) + session.commit() + + x = yield self.proxy('routes',['abcdef123456789','sms'], code=200) + self.failUnlessEqual(x, ['sipgate_basic','sipgate_plus']) + x = yield self.proxy('routes', ['abcdef123456789','fax'], code=200) + self.failUnlessEqual(x, []) + + with self.session() as session: + u = session.query(User).filter_by(name="test").first() + u.rights.append(Userright(o)) + + x = yield self.proxy('routes',['abcdef123456789','sms'], code=200) + self.failUnlessEqual(x, ['sipgate_basic','sipgate_plus']) + x = yield self.proxy('routes', ['abcdef123456789','fax'], code=200) + self.failUnlessEqual(x, ['faxde']) + + def testDefaultRoutes(self): + '''test the defaultRoute function''' + with self.session() as session: + u=User(name='test',apikey='abcdef123456789') + o=Offer(name="sipgate_basic", provider="sipgate", route="basic", typ="sms") + u.rights.append(Userright(o,True)) + o=Offer(name="sipgate_plus", provider="sipgate", route="plus", typ="sms") + u.rights.append(Userright(o)) + session.add(u) + d = self.proxy('defaultRoute', ['abcdef123456789','sms'], code=200) + d.addCallback(lambda x: self.failUnlessEqual(x,['sipgate_basic'])) + + return d + + def testTelnumbers(self): + '''test the telefon validator''' + dl = [] + d = self.proxy('telnumber',[["0123/456(78)","+4912346785433","00123435456-658"]], code=200) + d.addCallback(lambda x: self.failUnlessEqual(x,True)) + dl.append(d) + + invalid=['xa','+1','1-23',';:+0','0123'] + def f(exc): + self.failUnlessEqual(exc.faultCode, 701) + self.failUnlessEqual(exc.faultString, "No valid telnumber: '%s'" % invalid[0]) + d = self.proxy('telnumber',[['01234']+invalid], code=400) + d = self.assertFailure(d, Fault) + d.addCallback(f) + dl.append(d) + + return defer.DeferredList(dl, fireOnOneErrback=True) + + def testVaildEmail(self): + '''test vaild email adresses (got from wikipedia)''' + validmails=["niceandsimple@example.com"] + d = self.proxy('email', {"recipients":validmails}, code=200) + d.addCallback(lambda x: self.failUnlessEqual(x,True)) + return d + + def testInvaildEmail(self): + '''test invaild email adresses (got from wikipedia)''' + invalid=["Abc.example.com",'foo@t.de'] + def f(exc): + self.failUnlessEqual(exc.faultCode, 702) + self.failUnlessEqual(exc.faultString, "No valid email: '%s'" % invalid[0]) + d = self.proxy('email', [invalid], code=400) + d = self.assertFailure(d, Fault) + d.addCallback(f) + return d + + def testBill(self): + '''test bill function''' + apikey='abcdef123456789' + with self.session() as session: + u=User(name='test',apikey=apikey) + session.add(u) + d = self.proxy('bill', {"user":apikey}, code=200) + d.addCallback(lambda x: self.failUnlessEqual(x,{'total':{'price':0.0,'anz':0}})) + + return d + + def testBillWithPrice(self): + apikey='abcdef123456789' + with self.session() as session: + u=User(name='test',apikey=apikey) + session.add(u) + o = Offer(name='sipgate_basic',provider="sipgate",route="basic",typ="sms") + u.rights.append(Userright(o)) + j = Job(info='i',status='sended') + j.messages.append(Message(recipient='0123456789', isBilled=False, date=datetime.now() , price=0.4, offer=o)) + u.jobs.append(j) + + j = Job(info='a',status='sended') + j.messages.append(Message(recipient='0123456789', isBilled=False, date=datetime.now(), price=0.4, offer=o)) + u.jobs.append(j) + + def f(ret): + self.failUnlessEqual(ret['total'],{'price':0.8,'anz':2}) + self.failUnlessEqual(ret['sipgate_basic'], + {'price':0.8,'anz':2, + 'info':{'i':{'price':0.4,'anz':1}, + 'a':{'price':0.4,'anz':1}, + } + }) + + d = self.proxy('bill', [apikey], code=200) + d.addCallback(f) + return d + +if __name__ == '__main__': + unittest.main() diff -r a73bbc1d8b4b -r 93686b0c028b iro/view/jsonresource.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/iro/view/jsonresource.py Thu Sep 27 14:25:41 2012 +0200 @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 netzguerilla.net +# +# This file is part of Iro. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +# #Software, and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +# INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +# PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +from twisted.web import resource, server, http +from twisted.python import log, failure +from twisted.internet import defer + +from ..controller.viewinterface import Interface +from ..error import ValidateException + +try: + import json +except ImportError: + import simplejson as json + + +class TwistedInterface(Interface): + """Class that addes needed function for JSON""" + def __init__(self): + Interface.__init__(self) + + def listMethods(self): + """Since we override lookupProcedure, its suggested to override + listProcedures too. + """ + return self.listProcedures() + + + def listProcedures(self): + """returns a list of all functions that are allowed to call via XML-RPC.""" + return ['listMethods','status','sms','fax','mail','routes','defaultRoute','bill','telnumber','email'] + +class MethodFactory(resource.Resource): + def __init__(self,method,twistedInterface): + self.method = method + self.twistedInterface = twistedInterface + + def render(self,request): + try: + args = [] + if request.getHeader('Content-Type') == 'application/x-www-form-urlencoded': + args = {} + for a in request.args: + value = request.args[a] + if a != "recipients" and len(value) == 1: + value = value[0] + args[a] = value + elif request.getHeader('Content-Type') == 'application/json': + content = request.content.read() + if content: + args = json.loads(content) + if args is None: + args = [] + else: + request.setResponseCode(http.NOT_ACCEPTABLE) + return "Only application/x-www-form-urlencoded or application/json ist allowed for Content-Type" + if isinstance(args,list): + d = defer.maybeDeferred(getattr(self.twistedInterface,self.method),*args) + else: + d = defer.maybeDeferred(getattr(self.twistedInterface,self.method), **args) + d.addCallback(self._cbRender, request) + d.addErrback(self._ebRender, request) + d.addBoth(lambda _: request.finish()) + return server.NOT_DONE_YET + except Exception: + log.err(failure.Failure()) + request.setResponseCode(http.INTERNAL_SERVER_ERROR) + err= { + "code" : 999, + "msg" : "Unknown error.", + } + request.setHeader('Content-Type', 'application/json') + return json.dumps({"status":False, "error":err}) + + + def _cbRender(self,result,request): + request.setHeader('Content-Type', 'application/json') + request.write(json.dumps({"status":True, "result":result})) + + def _ebRender(self, failure, request): + if isinstance(failure.value, ValidateException): + request.setResponseCode(http.BAD_REQUEST) + else: + request.setResponseCode(http.INTERNAL_SERVER_ERROR) + + err= { + "code" : 999, + "msg" : "Unknown error.", + } + + try: + err["code"]=failure.value.code + err["msg"]=failure.value.msg + except Exception: + log.err(failure) + pass + request.setHeader('Content-Type', 'application/json') + request.write(json.dumps({"status":False, "error":err})) + +class JSONFactory(resource.Resource): + """JSON factory""" + def __init__(self): + resource.Resource.__init__(self) + self.twistedInterface = TwistedInterface() + for method in self.twistedInterface.listProcedures(): + self.putChild(method, MethodFactory(method, self.twistedInterface)) + + +def appendResource(root): + """adding JSON to root.""" + root.putChild('json', JSONFactory()) + +if __name__ == '__main__': + from twisted.web import resource + from twisted.internet import reactor + + root = resource.Resource() + root = appendResource(root) + reactor.listenTCP(7080, server.Site(root)) + reactor.run()