Permalink
Browse files

First running version.

  • Loading branch information...
0 parents commit 9f45b69abd4966175d8dcc881679b1dbc05a62e8 @abourget committed Feb 11, 2011
Showing with 413 additions and 0 deletions.
  1. +3 −0 .gitignore
  2. +131 −0 README.txt
  3. +156 −0 pyramid_socketio/__init__.py
  4. +70 −0 pyramid_socketio/serve.py
  5. +11 −0 pyramid_socketio/servereload.py
  6. +42 −0 setup.py
3 .gitignore
@@ -0,0 +1,3 @@
+*~
+*.pyc
+pyramid_socketio.egg-info/
131 README.txt
@@ -0,0 +1,131 @@
+Gevent-based Socket.IO integration for Pyramid (and WSGI frameworks)
+====================================================================
+
+Simple usage:
+
+<pre>
+### somewhere in a Pyramid view:
+from intr.socketio import SocketIOContext, socketio_manage
+
+
+class ConnectIOContext(SocketIOContext):
+ """Starting context, which will go one side or the other"""
+ def msg_connect(self, msg):
+ if msg.get('context') not in contexts:
+ self.io.send(dict(type="error", error="unknown_connect_context",
+ msg="You asked for a context that doesn't exist"))
+ return
+ # Waiting for a msg such as: {'type': connect', 'context': 'interest'}
+ newctx = self.switch(contexts[msg['context']])
+ if hasattr(newctx, 'startup'):
+ newctx.startup(msg)
+ # Returning a new IOContext switches the WebSocket context, and will
+ # call this context's methods for next incoming messages.
+ return newctx
+
+ def msg_login(self, msg):
+ # Do the login, then wait for the next connect
+ from intr.bound_models import User, ObjectId
+ u = User.find_one({'_id': ObjectId("4d4892a12a16e62df4000000")})
+ intrs = u['interests']
+ from intr.views.auth import create_session
+ create_session(request, u, intrs)
+ print "Logged, created session"
+
+
+class InterestIOContext(SocketIOContext):
+ def startup(self, connect_msg):
+ print "Started the interest context"
+ self.intr_id = connect_msg['interest_id']
+ # TODO: make sure we don't leak Sessions from MongoDB!
+ from intr.models import mdb # can't import globally, because of Pyramid
+ self.db = mdb
+ self.conn = BrokerConnection("localhost", "guest", "guest", "/")
+ self.chan = self.conn.channel()
+ self.queue = Queue("session-%s" % self.io.session.session_id,
+ exchange=intr_exchange,
+ durable=False, exclusive=True,
+ auto_delete=True,
+ routing_key="interest.%s" % self.intr_id)
+
+ self.producer = Producer(self.chan, exchange=intr_exchange,
+ serializer="json",
+ routing_key="interest.%s" % self.intr_id)
+ self.producer.declare()
+ self.consumer = Consumer(self.chan, [self.queue])
+ self.consumer.declare()
+ self.consumer.register_callback(self.consume_queue_message)
+ self.spawn(self.queue_recv)
+
+ # Do we need this ? Please fix the session instead, have a new one
+ # init'd for each incoming msg, or when calling save(), re-create a new
+ # SessionObject.
+ request = self.request
+ self.user = request.session['user']
+ self.temporary = request.session['temporary']
+ self.user_id = request.session['user_id']
+
+ def consume_queue_message(self, body, message):
+ """Callback when receiving anew message from Message Queue"""
+ # Do something when received :)
+ print "Received message from queue:", self.io.session.session_id, body
+ self.io.send(body)
+
+ def queue_recv(self):
+ """Wait for messages from Queue"""
+ self.consumer.consume(no_ack=True)
+ # consume queue...
+ while True:
+ gevent.sleep(0)
+ self.conn.drain_events()
+ if not self.io.connected():
+ return
+
+ #
+ # Socket messages
+ #
+ def msg_memorize(self, msg):
+ # do something
+
+ def msg_forget(self, msg):
+ pass
+
+ def msg_interest(self, msg):
+ pass
+
+ def msg_change_privacy(self, msg):
+ pass
+
+ def msg_get_members(self, msg):
+ pass
+
+
+
+
+contexts = {'interest': InterestIOContext,
+ 'somewhereelse': SocketIOContext,
+ }
+
+
+
+#
+# SOCKET.IO implementation
+#
+@view_config(route_name="socket_io")
+def socket_io(request):
+ """Deal with the SocketIO protocol, using SocketIOContext objects"""
+ # Offload management to the pyramid_socketio module
+
+ retval = socketio_manage(ConnectIOContext(request))
+ #print "socketio_manage ended"
+ return Response(retval)
+
+
+
+#### Inside __init__.py for your Pyramid application:
+def main(..):
+ ...
+ config.add_static_view('socket.io/lib', 'intr:static')
+ config.add_route('socket_io', 'socket.io/*remaining')
+ ....
+</pre>
156 pyramid_socketio/__init__.py
@@ -0,0 +1,156 @@
+# -=- encoding: utf-8 -=-
+
+import logging
+import gevent
+
+__all__ = ['SocketIOError', 'SocketIOContext',
+ 'socketio_manage']
+
+log = logging.getLogger(__name__)
+
+class SocketIOError(Exception):
+ pass
+
+class SocketIOKeyAssertError(SocketIOError):
+ pass
+
+class SocketIOContext(object):
+ def __init__(self, request):
+ """Created by the call to connect() before doing any generic recv()"""
+ self.request = request
+ self.io = request.environ['socketio']
+ self._parent = None
+ if not hasattr(request, 'jobs'):
+ request.jobs = []
+ # Override self.debug if in production mode
+ #self.debug = lambda x: None
+
+ def debug(self, msg):
+ log.debug("%s: %s" % (self.io.session.session_id, msg))
+
+ def spawn(self, callable, *args):
+ """Spawn a new process in the context of this request.
+
+ It will be monitored by the "watcher" method
+ """
+ self.debug("Spawning greenlet: %s" % callable.__name__)
+ new = gevent.spawn(callable, *args)
+ self.request.jobs.append(new)
+ return new
+
+ def kill(self):
+ """Kill the current context, pass control to the parent context if
+ "return" is True. If this is the last context, close the connection."""
+ # Detach objects to dismantle cyclic references
+ # (was that going to happen anyway ?)
+ request = self.request
+ io = self.io
+ self.request = None
+ self.io = None
+ if self._parent:
+ parent = self._parent
+ self._parent = None
+ return parent
+ else:
+ io.close()
+ return
+
+ def switch(self, new_context):
+ """Switch context, stack up contexts and pass on request, the caller
+ must return the value returned by switch().
+ """
+ self.debug("Switching context: %s" % new_context.__name__)
+ newctx = new_context(self.request)
+ newctx._parent = self
+ return newctx
+
+ def error(self, code, msg):
+ """Used to quickly generate an error message"""
+ self.debug("error: %s, %s" % (code, msg))
+ self.io.send(dict(type='error', error=code, msg=msg))
+
+ def msg(self, msg_type, **kwargs):
+ """Used to quickly generate an error message"""
+ self.debug("message: %s, %s" % (msg_type, kwargs))
+ self.io.send(dict(type=msg_type, **kwargs))
+
+ def assert_keys(self, msg, elements):
+ """Make sure the elements are inside the message, otherwise send an
+ error message and skip the message.
+ """
+ if isinstance(elements, (str, unicode)):
+ elements = (elements,)
+ for el in elements:
+ if el not in msg:
+ self.error("bad_request", "Msg type '%s' should include all those keys: %s" % (msg['type'], elements))
+ raise SocketIOKeyAssertError()
+
+ def __call__(self, msg):
+ """Parse the message upon reception and dispatch it to the good method.
+ """
+ msg_type = "msg_" + msg['type']
+ if not hasattr(self, msg_type) or \
+ not callable(getattr(self, msg_type)):
+ self.error("unknown_command", "Command unknown: %s" % msg['type'])
+ return
+ try:
+ self.debug("Calling msg type: %s with obj: %s" % (msg_type, msg))
+ return getattr(self, msg_type)(msg)
+ except SocketIOKeyAssertError, e:
+ return None
+
+
+def watcher(request):
+ """Watch if any of the greenlets for a request have died. If so, kill the request and the socket.
+ """
+ # TODO: add that if any of the request.jobs die, kill them all and exit
+ io = request.environ['socketio']
+ gevent.sleep(5.0)
+ while True:
+ gevent.sleep(1.0)
+ if not io.connected():
+ gevent.killall(request.jobs)
+ return
+
+def socketio_recv(context):
+ """Manage messages arriving from Socket.IO, dispatch to context handler"""
+ io = context.io
+ while True:
+ for msg in io.recv():
+ # Skip invalid messages
+ if not isinstance(msg, dict):
+ context.error("bad_request",
+ "Your message needs to be JSON-formatted")
+ elif 'type' not in msg:
+ context.error("bad_request",
+ "You need a 'type' attribute in your message")
+ else:
+ # Call msg in context.
+ newctx = context(msg)
+
+ # Switch context ?
+ if newctx:
+ context = newctx
+
+ if not io.connected():
+ return
+
+def socketio_manage(start_context):
+ """Main SocketIO management function, call from within your Pyramid view"""
+ request = start_context.request
+ io = request.environ['socketio']
+
+ if not io.connected():
+ # probably asked for something else dude!
+ return "there's no reason to get here, you won't get any further. have you mapped socket.io/lib to something ?"
+
+ start_context.spawn(socketio_recv, start_context)
+
+ # Launch the watcher thread
+ killall = gevent.spawn(watcher, request)
+
+ gevent.joinall(request.jobs + [killall])
+
+ start_context.debug("socketio_manage terminated")
+
+ return "done"
70 pyramid_socketio/serve.py
@@ -0,0 +1,70 @@
+#!/usr/bin/env python
+import gevent
+from gevent import monkey; monkey.patch_all()
+
+from ConfigParser import ConfigParser
+import logging
+import logging.config
+import socket
+import sys
+import os
+
+from socketio import SocketIOServer
+from paste.deploy import loadapp
+
+
+host = '127.0.0.1'
+port = 6543
+
+def socketio_serve():
+ # See http://bitbucket.org/Jeffrey/socketio/src/9bf2cd777808/examples/chat.py
+
+ if len(sys.argv) < 2:
+ print "ERROR: Please specify .ini file on command line"
+ sys.exit(1)
+
+ do_reload = sys.argv[1] == '--reload'
+
+ # Setup logging...
+ cfgfile = sys.argv[2] if do_reload else sys.argv[1]
+ logging.config.fileConfig(cfgfile)
+ log = logging.getLogger(__name__)
+
+ cfg = ConfigParser()
+ cfg.readfp(open(cfgfile))
+ sec = 'server:main'
+ if sec in cfg.sections():
+ opts = cfg.options(sec)
+ if 'host' in opts:
+ host = cfg.get(sec, 'host')
+ if 'port' in opts:
+ port = cfg.getint(sec, 'port')
+
+ def main():
+ # Load application and config.
+ app = loadapp('config:%s' % cfgfile, relative_to='.')
+ server = SocketIOServer((host, port), app,
+ resource="socket.io")
+
+ try:
+ print "Serving on %s:%d (http://127.0.0.1:%d) ..." % (host, port, port)
+ server.serve_forever()
+ except socket.error, e:
+ print "ERROR SERVING WSGI APP: %s" % e
+ sys.exit(1)
+
+ def reloader():
+ from paste import reloader
+ reloader.install()
+ reloader.watch_file(cfgfile)
+ import glob # Restart on "compile_catalog"
+ # TODO: make more generic, and more robust
+ for lang in glob.glob('*/locale/*/LC_MESSAGES/*.mo'):
+ reloader.watch_file(lang)
+ for lang in glob.glob('*/i18n/*/LC_MESSAGES/*.mo'):
+ reloader.watch_file(lang)
+
+ jobs = [gevent.spawn(main)]
+ if do_reload:
+ jobs.append(gevent.spawn(reloader))
+ gevent.joinall(jobs)
11 pyramid_socketio/servereload.py
@@ -0,0 +1,11 @@
+#!/usr/bin/env python
+
+import os
+import sys
+
+def socketio_serve_reload():
+ """Spawn a new process and reload when it dies"""
+ while True:
+ ret = os.system("socketio-serve %s" % (sys.argv[1]))
+ if ret != 3:
+ break
42 setup.py
@@ -0,0 +1,42 @@
+import os
+import sys
+
+from setuptools import setup, find_packages
+
+here = os.path.abspath(os.path.dirname(__file__))
+#README = open(os.path.join(here, 'README.txt')).read()
+#CHANGES = open(os.path.join(here, 'CHANGES.txt')).read()
+
+requires = [
+ 'pyramid',
+ 'gevent',
+ 'gevent-socketio',
+ 'gevent-websocket',
+ 'greenlet',
+ ]
+
+setup(name='pyramid_socketio',
+ version='0.1',
+ description='Gevent-based Socket.IO pyramid integration and helpers',
+ #long_description=README + '\n\n' + CHANGES,
+ classifiers=[
+ "Programming Language :: Python",
+ "Framework :: Pylons",
+ "Framework :: Pyramid",
+ "Topic :: Internet :: WWW/HTTP",
+ ],
+ author='Alexandre Bourget',
+ author_email='alex@bourget.cc',
+ url='http://blog.abourget.net',
+ keywords='web wsgi pylons pyramid websocket python gevent socketio socket.io',
+ packages=find_packages(),
+ include_package_data=True,
+ zip_safe=False,
+ install_requires = requires,
+ entry_points = """\
+ [console_scripts]
+ socketio-serve-reload = pyramid_socketio.servereload:socketio_serve_reload
+ socketio-serve = pyramid_socketio.serve:socketio_serve
+ """,
+ )
+

0 comments on commit 9f45b69

Please sign in to comment.