Commits

Alexander Shorin committed 9fd0c16

Support endpoint as coroutine

The initial design was good for single request -> single/multiple responses,
but is completely unsuitable for situations, when you need make additional
request from endpoint and handle response on it before return any result to
client.

For instance, you're trying to retrieve version info from some user in MUC.
With coroutines this would be pretty simple task:

@app.route('version <nick>')
def version(nick):
room = request.room
if not room or nick not in room:
yield 'who is it?'
return
res = yield room[nick].version_info() # request-response goes behind of scene
if res is None:
yield "he's hiding from NSA"
else:
yield '{name} {version} {os}'.format(**res)

Note: example above isn't working for current commit, but soon similar will be.

Comments (0)

Files changed (7)

xmppflask/ext/sleekxmpp.py

 
 from ..server import XmppWsgiServerInterface, maybe_unicode
 
+
 class SleekXmppWsgiInterface(XmppWsgiServerInterface):
     """XMPPWSGI interface based on SleekXMPP library."""
+
     def __init__(self):
         sleekxmpp = __import__('sleekxmpp')
         self.module = sleekxmpp
             delay = time.mktime(
                 event.plugins['delay'].get_stamp().utctimetuple()
             )
-            environ['XMPP_DELAY'] =  environ['XMPP_TIMESTAMP'] - delay
+            environ['XMPP_DELAY'] = environ['XMPP_TIMESTAMP'] - delay
 
         if environ['from']:
             environ['XMPP_JID'] = environ['from'].split('/')[0]
         self.client.send_message(mto=payload['to'],
                                  mbody=payload['body'],
                                  mtype=environ['type'])
+        return True
 
     def send_presence(self, environ, payload):
         """Sends XMPP presence event.
             self.client.send_presence(pto=to_jid,
                                       pstatus=payload.get('status', ''),
                                       pshow=payload['type'])
+        return True

xmppflask/ext/xmpppy.py

 
 from ..server import XmppWsgiServerInterface, maybe_unicode
 
+
 class XmpppyWsgiInterface(XmppWsgiServerInterface):
     """XMPPWSGI interface based on xmpppy library."""
+
     def __init__(self):
         import xmpp
+
         self.module = xmpp
         self.message_class = xmpp.Message
         self.presence_class = xmpp.Presence
                 datetime.datetime.strptime(delay,
                                            '%Y%m%dT%H:%M:%S').utctimetuple()
             )
-            environ['XMPP_DELAY'] =  environ['XMPP_TIMESTAMP'] - delay
+            environ['XMPP_DELAY'] = environ['XMPP_TIMESTAMP'] - delay
 
         if environ['from']:
             environ['XMPP_JID'] = environ['from'].split('/')[0]
 
         if isinstance(event, self.message_class):
-            environ['body'] = environ['MESSAGE'] = maybe_unicode(event.getBody())
+            environ['body'] = environ['MESSAGE'] = maybe_unicode(
+                event.getBody())
             environ['subject'] = maybe_unicode(event.getSubject())
             if environ['type'] == 'groupchat':
                 environ['mucroom'] = environ['from'].split('/')[0]
         self.client.send(self.message_class(jid,
                                             payload['body'],
                                             typ=environ['type']))
+        return True
 
     def send_presence(self, environ, payload):
         """Sends XMPP presence event.
                                            status=payload.get('status', ''),
                                            show=payload['type'])
         self.client.send(presence)
+        return True
 
     def dispatch_app_response(self, environ, response):
         """Response object dispatcher."""
-        for item in response:
-            if isinstance(item, basestring):
-                cmd, payload = 'message', item
+        rv = None
+        while True:
+            try:
+                item = response.send(rv)
+            except StopIteration:
+                break
             else:
-                cmd, payload = item
-            if cmd == 'message' and payload:
-                func = self.send_message
-            elif cmd == 'presence':
-                func = self.send_presence
-            elif cmd == 'iq':
-                func = self.send_iq
-            else:
-                raise ValueError('unknown command %r' % cmd)
-            func(environ, payload)
+                if isinstance(item, basestring):
+                    cmd, payload = 'message', item
+                else:
+                    cmd, payload = item
+                if cmd == 'message' and payload:
+                    func = self.send_message
+                elif cmd == 'presence':
+                    func = self.send_presence
+                elif cmd == 'iq':
+                    func = self.send_iq
+                else:
+                    raise ValueError('unknown command %r' % cmd)
+                rv = func(environ, payload)
 
     def handle_message(self, message):
         """Handles message events."""

xmppflask/tests/helpers.py

                 'type': environ.get('type', 'chat'),
             }
         ))
+        return True
 
     def send_presence(self, environ, payload):
         if 'status' in payload:
                     'type': payload['type']
                 }
             ))
+        return True
 
 

xmppflask/tests/test_server.py

         self.assertEqual(payload['body'], 'PONG!')
         self.assertEqual(payload['type'], 'chat')
 
+    def test_dispatch_coroutine(self):
+        def ping():
+            assert (yield 'presence', {'type': 'foo'})
+            assert (yield 'presence', {'type': 'bar'})
+            assert (yield 'presence', {'type': 'baz'})
+            assert (yield 'PONG!')
+
+        environ = {'XMPP_JID': 'foo@bar'}
+        resp = self.server.app.response_class(ping())
+
+        self.server.dispatch_app_response(environ, resp)
+
+        queue = self.server.xmpp.m_queue
+        self.assertEqual(len(queue), 4)
+
+        seq = ['foo', 'bar', 'baz']
+        for idx, type_ in enumerate(seq):
+            cmd, payload = queue[idx]
+            self.assertEqual(cmd, 'presence')
+            self.assertEqual(payload['to'], environ['XMPP_JID'])
+            self.assertEqual(payload['type'], type_)
+            if idx == 2:
+                break
+
+        cmd, payload = queue[-1]
+        self.assertEqual(cmd, 'message')
+        self.assertEqual(payload['to'], environ['XMPP_JID'])
+        self.assertEqual(payload['body'], 'PONG!')
+        self.assertEqual(payload['type'], 'chat')
 
 if __name__ == '__main__':
     unittest.main()

xmppflask/tests/test_wrappers.py

         self.assertEqual(gen.next(), 1)
         self.assertEqual(resp.next(), 2)
         self.assertRaises(StopIteration, gen.next)
+
+    def test_coroutine(self):
+        def gen():
+             ok = yield 'foo'
+             assert ok
+             ok = yield 'bar'
+             assert ok
+        resp = Response(gen())
+        self.assertEqual(resp.send(None), 'foo')
+        self.assertEqual(resp.send(True), 'bar')
+        self.assertRaises(StopIteration, resp.send, True)

xmppflask/wrappers.py

     :license: BSD.
 """
 
-from itertools import chain
-from collections import Iterable
+from collections import Iterable, Iterator, Callable
+from types import GeneratorType
+
 
 class Request(object):
+
     #: if matching the route failed, this is the exception that will be
     #: raised / was raised as part of the request handling.  This is
     #: usually a :exc:`~xmppflask.exceptions.NotFound` exception or
     #: something similar.
     routing_exception = None
-    
+
     #: the internal route rule that matched the request.  This can be
     #: useful to inspect which methods are allowed for the URL from
     #: a before/after handler (``request.url_rule.methods``) etc.
     url_rule = None
-    view_args = None 
-   
+    view_args = None
+
     def __init__(self, environ):
         self.environ = environ
-    
+
     @property
     def endpoint(self):
         """The endpoint that matched the request.  This in combination with
         """
         if self.url_rule is not None:
             return self.url_rule.endpoint
-    
+
     @property
     def blueprint(self):
         """The name of the current blueprint"""
             return self.url_rule.endpoint.rsplit('.', 1)[0]
 
 
-class Response(object):
+class Response(Iterator, Callable):
     """Response object which implements iterator interface."""
 
-    def __init__(self, data=None):
-        if isinstance(data, basestring):
-            data = [data]
-        elif data is None:
-            data = []
-        elif isinstance(data, (tuple, list)) and len(data) == 2:
-            data = [data]
-        elif not isinstance(data, Iterable):
-            data = [str(data)]
-        self.data = (item for item in data)
+    def __init__(self, data):
+        self._stack = []
+        self._current = self._wrap(data)
 
     def __call__(self, other):
-        self.data = chain(self.data, other)
+        self._stack.append(self._wrap(other))
         return self
 
     def __iter__(self):
         return self
 
+    def _wrap(self, data):
+        if not isinstance(data, GeneratorType):
+            if isinstance(data, basestring):
+                data = [data]
+            elif data is None:
+                data = []
+            elif isinstance(data, (tuple, list)) and len(data) == 2:
+                data = [data]
+            elif not isinstance(data, Iterable):
+                data = [str(data)]
+            return (item for item in data)
+        return data
+
     def next(self):
-        return self.data.next()
+        return self.send(None)
+
+    def send(self, value):
+        try:
+            return self._current.send(value)
+        except StopIteration:
+            if self._stack:
+                self._current = self._stack.pop(0)
+                return self.next()
+            else:
+                raise
+
+    def throw(self, exc_type, exc_val=None, exc_tb=None):
+        resp = self._current.throw(exc_type, exc_val, exc_tb)
+        if resp is not None:
+            return resp
+
+    def close(self):
+        self._current.close()