Permalink
Browse files

initial commit

  • Loading branch information...
0 parents commit 85534ef0f802a3ee0a38287921d83f9fbc446956 @guyzmo committed May 26, 2012
163 README.md
@@ -0,0 +1,163 @@
+Event Source Library for Python
+===============================
+
+This library implements W3C Draft's on event-source:
+ * http://dev.w3.org/html5/eventsource/
+
+It enables a halfduplex communication from server to client, but initiated
+by the client, through standard HTTP(S) communication.
+
+Dependances
+===========
+
+ - Fairly recent python (tested with 2.7)
+ - Fairly recent tornado (tested with 2.2.1)
+
+Usage
+=====
+
+ 1. Launch the server:
+
+ python event_source/event_source_listener.py -P 8888 -i -k 50000
+
+ 2. Launch the client:
+
+ python event_source/event_source_client.py 69:69:69:69:69:69 -r 5000
+
+ 3. Send requests:
+
+ python event_source/send_request.py 69:69:69:69:69:69 ping "42"
+ python event_source/send_request.py 69:69:69:69:69:69 close
+
+Command Line arguments
+======================
+
+ - event_source_listener:
+
+ usage: event_source/event_source_listener.py [-h] [-H HOST] [-P PORT] [-d]
+ [-j] [-k KEEPALIVE] [-i]
+
+ Event Source Listener
+
+ optional arguments:
+ -h, --help show this help message and exit
+ -H HOST, --host HOST Host to bind on
+ -P PORT, --port PORT Port to bind on
+ -d, --debug enables debug output
+ -j, --json to enable JSON Event
+ -k KEEPALIVE, --keepalive KEEPALIVE
+ Keepalive timeout
+ -i, --id to generate identifiers
+
+ - event_source_client:
+
+ usage: event_source/event_source_client.py [-h] [-H HOST] [-P PORT] [-d]
+ [-r RETRY]
+ token
+
+ Event Source Client
+
+ positional arguments:
+ token Token to be used for connection
+
+ optional arguments:
+ -h, --help show this help message and exit
+ -H HOST, --host HOST Host to connect to
+ -P PORT, --port PORT Port to be used connection
+ -d, --debug enables debug output
+ -r RETRY, --retry RETRY
+ Reconnection timeout
+
+ - send_request:
+
+ usage: event_source/send_request.py [-h] [-H HOST] [-P PORT] [-j]
+ token action [data]
+
+ Generates event for Event Source Library
+
+ positional arguments:
+ token Token to be used for connection
+ action Action to send
+ data Data to be sent
+
+ optional arguments:
+ -h, --help show this help message and exit
+ -H HOST, --host HOST Host to connect to
+ -P PORT, --port PORT Port to be used connection
+ -j, --json Treat data as JSON
+
+
+Integrate
+=========
+
+On the server side, basically all you have to do is to add the following to your code:
+
+ from event_source import event_source_listener
+
+ application = tornado.web.Application([
+ (r"/(.*)/(.*)", event_source_listener.EventSourceHandler,
+ dict(event_class=EVENT,
+ keepalive=KEEPALIVE)),
+ ])
+
+ application.listen(PORT)
+ tornado.ioloop.IOLoop.instance().start()
+
+where:
+ - PORT is an integer for the port to bind to
+ - KEEPALIVE is an integer for the timeout between two keepalive messages (to protect from disconnections)
+ - EVENT is a event_source_listener.Event based class, either one you made or
+ - event_source_listener.StringEvent : Each event gets and resends multiline strings
+ - event_source_listener.StringIdEvent : Each event gets and resends multiline strings, with an unique id for each event
+ - event_source_listener.JSONEvent : Each event gets and resends JSON valid strings
+ - event_source_listener.JSONIdEvent : Each event gets and resends JSON valid string, with an unique id for each event
+
+Extend
+======
+
+To extend the behaviour of the event source library, without breaking event_source
+definition, the Event based classes implements all processing elements that shall
+be done on events.
+
+There is two abstract classes that defines Event:
+ - event_source_listener.Event : defines the constructor of an Event
+ - event_source_listener.EventId : defines an always incrementing id handler
+
+here is an example to create a new Event that takes multiline data and join it in a one
+line string seperated with semi-colons.
+
+ class OneLineEvent(Event):
+ ACTIONS = ["ping",Event.FINISH]
+
+ """Property to enable multiline output of the value"""
+ def get_value(self):
+ # replace carriage returns by semi-colons
+ # this method shall always return a list (even if one value)
+ return [";".join([line for line in self._value.split('\n')])]
+
+ value = property(get_value,set_value)
+
+And now, I want to add basic id support to OneLineEvent, in OneLineIdEvent,
+nothing is easier :
+
+ class OneLineIdEvent(OneLineEvent,IdEvent):
+ id = property(IdEvent.get_value)
+
+Or if I want the id to be a timestamp:
+
+ import time
+ class OneLineTimeStampEvent(OneLineEvent):
+ id = property(lambda s: "%f" % (time.time(),))
+
+You can change the behaviour of a few things in a Event-based class:
+ - Event.LISTEN contains the GET action to open a connection (per default "poll")
+ - Event.FINISH contains the POST action to close a connection (per default "close")
+ - Event.RETRY contains the POST action to define the timeout after reconnecting on network disconnection (per default "0", which means disabled)
+ - in the Event.ACTIONS list, you define what POST actions are allowed, per default, only Event.FINISH is allowed.
+ - Event.content_type contains the "content_type" that will be asked for every form (it is not enforced).
+
+To change the way events are generated, you can directly call EventSourceHandler.buffer_event()
+to create a new event to be sent. But the post action is best, at least while WSGI can't handle
+correctly long polling connections.
+
+EOF
No changes.
@@ -0,0 +1,139 @@
+import sys
+import time
+import argparse
+import logging
+log = logging.getLogger('eventsource.client')
+
+from tornado.ioloop import IOLoop
+from tornado.httpclient import AsyncHTTPClient, HTTPRequest
+
+class Event(object):
+ """
+ Defines a received event
+ """
+ def __init__(self):
+ self.name = None
+ self.data = None
+ self.id = None
+
+ def __repr__(self):
+ return "Event<%s,%s,%s>" % (str(self.id), str(self.name), str(self.data.replace('\n','\\n')))
+
+class EventSourceClient(object):
+ def __init__(self,url,action,target,callback=None,retry=-1):
+ self._url = "http://%s/%s/%s" % (url,action,target)
+ AsyncHTTPClient.configure("tornado.curl_httpclient.CurlAsyncHTTPClient")
+ self.http_client = AsyncHTTPClient()
+ self.http_request = HTTPRequest(url=self._url,
+ method='GET',
+ headers={"content-type":"text/event-stream"},
+ request_timeout=0,
+ streaming_callback=self.handle_stream)
+ if callback is None:
+ self.cb = lambda e: log.info( "received %s" % (e,) )
+ else:
+ self.cb = callback
+ self.retry_timeout = int(retry)
+
+ def poll(self):
+ if self.retry_timeout == 0:
+ self.http_client.fetch(self.http_request, self.handle_request)
+ IOLoop.instance().start()
+ while self.retry_timeout!=0:
+ self.http_client.fetch(self.http_request, self.handle_request)
+ IOLoop.instance().start()
+ time.sleep(self.retry_timeout/1000)
+
+ def end(self):
+ self.retry_timeout=0
+ IOLoop.instance().stop()
+
+ def handle_stream(self,message):
+ event = Event()
+ for line in message.strip('\r\n').split('\r\n'):
+ (field, value) = line.split(":",1)
+ if field == 'event':
+ event.name = value
+ elif field == 'data':
+ value = value.lstrip()
+ if event.data is None:
+ event.data = value
+ else:
+ event.data = "%s\n%s" % (event.data, value)
+ elif field == 'id':
+ event.id = value
+ elif field == 'retry':
+ try:
+ self.retry_timeout = int(value)
+ log.info( "timeout reset: %s" % (value,) )
+ except ValueError:
+ pass
+ elif field == '':
+ log.info( "received comment: %s" % (value,) )
+ else:
+ raise Exception("Unknown field !")
+ if event.name is not None:
+ self.cb(event)
+
+
+ def handle_request(self,response):
+ if response.error:
+ log.error(response.error)
+ else:
+ log.info("disconnection requested")
+ self.retry_timeout=0
+ IOLoop.instance().stop()
+
+def start():
+ parser = argparse.ArgumentParser(prog=sys.argv[0],
+ description="Event Source Client")
+ parser.add_argument("-H",
+ "--host",
+ dest="host",
+ default='127.0.0.1',
+ help='Host to connect to')
+ # PORT ARGUMENT
+ parser.add_argument("-P",
+ "--port",
+ dest="port",
+ default='8888',
+ help='Port to be used connection')
+
+ parser.add_argument("-d",
+ "--debug",
+ dest="debug",
+ action="store_true",
+ help='enables debug output')
+
+ parser.add_argument("-r",
+ "--retry",
+ dest="retry",
+ default='-1',
+ help='Reconnection timeout')
+
+ parser.add_argument(dest="token",
+ help='Token to be used for connection')
+
+ args = parser.parse_args(sys.argv[1:])
+
+ if args.debug:
+ logging.basicConfig(level=logging.DEBUG)
+ else:
+ logging.basicConfig(level=logging.INFO)
+
+ ###
+
+ def log_events(event):
+ log.info( "received %s" % (event,) )
+
+ EventSourceClient(url="%(host)s:%(port)s" % args.__dict__,
+ action="poll",
+ target=args.token,
+ retry=args.retry).poll()
+
+ ###
+
+
+if __name__ == "__main__":
+ start()
+
Oops, something went wrong.

0 comments on commit 85534ef

Please sign in to comment.