pax_global_header00006660000000000000000000000064122431602500014505gustar00rootroot0000000000000052 comment=68d9639a566d54c0ba52fd76b117aef1fecf2b5e socketIO-client-0.5.3/000077500000000000000000000000001224316025000145065ustar00rootroot00000000000000socketIO-client-0.5.3/.gitignore000066400000000000000000000001021224316025000164670ustar00rootroot00000000000000*~ *.sw[op] *.py[cod] *.egg *.egg-info build dist sdist .coverage socketIO-client-0.5.3/CHANGES.rst000066400000000000000000000030041224316025000163050ustar00rootroot000000000000000.5.3 ----- - Updated wait loop to exit if the client wants to disconnect - Fixed calling on_connect() so that it is called only once - Set heartbeat_interval to be half of the heartbeat_timeout 0.5.2 ----- - Replaced secure=True with host='https://example.com' - Fixed sending heartbeats thanks to Travis Odom 0.5.1 ----- - Added error handling in the event of websocket timeout - Fixed sending acknowledgments in custom namespaces thanks to Travis Odom 0.5 --- - Rewrote library to use coroutines instead of threads to save memory - Improved connection resilience - Added support for xhr-polling thanks to Francis Bull - Added support for jsonp-polling thanks to Bernard Pratz - Added support for query params and cookies 0.4 --- - Added support for custom headers and proxies thanks to Rui and Sajal - Added support for server-side callbacks thanks to Zac Lee - Added low-level _SocketIO to remove cyclic references - Merged Channel functionality into BaseNamespace thanks to Alexandre Bourget 0.3 --- - Added support for secure connections - Added socketIO.wait() - Improved exception handling in _RhythmicThread and _ListenerThread 0.2 --- - Added support for callbacks and channels thanks to Paul Kienzle - Incorporated suggestions from Josh VanderLinden and Ian Fitzpatrick 0.1 --- - Wrapped code from StackOverflow_ - Added exception handling to destructor in case of connection failure .. _StackOverflow: http://stackoverflow.com/questions/6692908/formatting-messages-to-send-to-socket-io-node-js-server-from-python-client socketIO-client-0.5.3/LICENSE000066400000000000000000000020641224316025000155150ustar00rootroot00000000000000Copyright (c) 2013 Roy Hyunjin Han and contributors 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. socketIO-client-0.5.3/MANIFEST.in000066400000000000000000000001071224316025000162420ustar00rootroot00000000000000recursive-include socketIO_client * include *.rst global-exclude *.pyc socketIO-client-0.5.3/README.rst000066400000000000000000000122151224316025000161760ustar00rootroot00000000000000socketIO-client =============== Here is a socket.io_ client library for Python. You can use it to write test code for your socket.io_ server. Installation ------------ :: VIRTUAL_ENV=$HOME/.virtualenv # Prepare isolated environment virtualenv $VIRTUAL_ENV # Activate isolated environment source $VIRTUAL_ENV/bin/activate # Install package pip install -U socketIO-client Usage ----- Activate isolated environment. :: VIRTUAL_ENV=$HOME/.virtualenv source $VIRTUAL_ENV/bin/activate For debugging information, run these commands first. :: import logging logging.basicConfig(level=logging.DEBUG) Emit. :: from socketIO_client import SocketIO with SocketIO('localhost', 8000) as socketIO: socketIO.emit('aaa') socketIO.wait(seconds=1) Emit with callback. :: from socketIO_client import SocketIO def on_bbb_response(*args): print 'on_bbb_response', args with SocketIO('localhost', 8000) as socketIO: socketIO.emit('bbb', {'xxx': 'yyy'}, on_bbb_response) socketIO.wait_for_callbacks(seconds=1) Define events. :: from socketIO_client import SocketIO def on_aaa_response(*args): print 'on_aaa_response', args socketIO = SocketIO('localhost', 8000) socketIO.on('aaa_response', on_aaa_response) socketIO.emit('aaa') socketIO.wait(seconds=1) Define events in a namespace. :: from socketIO_client import SocketIO, BaseNamespace class Namespace(BaseNamespace): def on_aaa_response(self, *args): print 'on_aaa_response', args self.emit('bbb') socketIO = SocketIO('localhost', 8000, Namespace) socketIO.emit('aaa') socketIO.wait(seconds=1) Define standard events. :: from socketIO_client import SocketIO, BaseNamespace class Namespace(BaseNamespace): def on_connect(self): print '[Connected]' socketIO = SocketIO('localhost', 8000, Namespace) socketIO.wait(seconds=1) Define different namespaces on a single socket. :: from socketIO_client import SocketIO, BaseNamespace class ChatNamespace(BaseNamespace): def on_aaa_response(self, *args): print 'on_aaa_response', args class NewsNamespace(BaseNamespace): def on_aaa_response(self, *args): print 'on_aaa_response', args socketIO = SocketIO('localhost', 8000) chat_namespace = socketIO.define(ChatNamespace, '/chat') news_namespace = socketIO.define(NewsNamespace, '/news') chat_namespace.emit('aaa') news_namespace.emit('aaa') socketIO.wait(seconds=1) Connect via SSL. :: from socketIO_client import SocketIO SocketIO('https://localhost') Specify params, headers, cookies, proxies thanks to the `requests`_ library. :: from socketIO_client import SocketIO from base64 import b64encode SocketIO('localhost', 8000, params={'q': 'qqq'}, headers={'Authorization': 'Basic ' + b64encode('username:password')}, cookies={'a': 'aaa'}, proxies={'https': 'https://proxy.example.com:8080'}) Wait forever. :: from socketIO_client import SocketIO socketIO = SocketIO('localhost') socketIO.wait() License ------- This software is available under the MIT License. Credits ------- - `Guillermo Rauch`_ wrote the `socket.io specification`_. - `Hiroki Ohtani`_ wrote websocket-client_. - rod_ wrote a `prototype for a Python client to a socket.io server`_ on StackOverflow. - `Alexandre Bourget`_ wrote gevent-socketio_, which is a socket.io server written in Python. - `Paul Kienzle`_, `Zac Lee`_, `Josh VanderLinden`_, `Ian Fitzpatrick`_, `Lucas Klein`_, `Rui Chicoria`_, `Travis Odom`_ submitted code to expand support of the socket.io protocol. - `Bernard Pratz`_ and `Francis Bull`_ wrote prototypes to support xhr-polling and jsonp-polling. - `Eric Chen`_, `Denis Zinevich`_, `Thiago Hersan`_, `Nayef Copty`_ suggested ways to make the connection more robust. .. _socket.io: http://socket.io .. _requests: http://python-requests.org .. _Guillermo Rauch: https://github.com/guille .. _socket.io specification: https://github.com/LearnBoost/socket.io-spec .. _Hiroki Ohtani: https://github.com/liris .. _websocket-client: https://github.com/liris/websocket-client .. _rod: http://stackoverflow.com/users/370115/rod .. _prototype for a Python client to a socket.io server: http://stackoverflow.com/questions/6692908/formatting-messages-to-send-to-socket-io-node-js-server-from-python-client .. _Alexandre Bourget: https://github.com/abourget .. _gevent-socketio: https://github.com/abourget/gevent-socketio .. _Bernard Pratz: https://github.com/guyzmo .. _Francis Bull: https://github.com/franbull .. _Paul Kienzle: https://github.com/pkienzle .. _Zac Lee: https://github.com/zratic .. _Josh VanderLinden: https://github.com/codekoala .. _Ian Fitzpatrick: https://github.com/GraphEffect .. _Lucas Klein: https://github.com/lukashed .. _Rui Chicoria: https://github.com/rchicoria .. _Travis Odom: https://github.com/burstaholic .. _Eric Chen: https://github.com/taiyangc .. _Denis Zinevich: https://github.com/dzinevich .. _Thiago Hersan: https://github.com/thiagohersan .. _Nayef Copty: https://github.com/nayefc socketIO-client-0.5.3/TODO.goals000066400000000000000000000001071224316025000163000ustar00rootroot00000000000000# US/Pacific 11/19/2013 Include graingert's pull request on travis.yml socketIO-client-0.5.3/TODO.log000066400000000000000000000003651224316025000157620ustar00rootroot00000000000000# UTC 11/19/2013 + Add nayefc to acknowledgments [11/19/2013] + Beware of scheme included in URL [11/17/2013] + Add test for server ack callback in namespace [11/17/2013] + Set port automatically if it is not automatically specified [11/17/2013]socketIO-client-0.5.3/serve_tests.js000066400000000000000000000044641224316025000174220ustar00rootroot00000000000000var io = require('socket.io').listen(8000); var main = io.of('').on('connection', function(socket) { socket.on('message', function(data, fn) { if (fn) { // Client expects a callback if (data) { fn(data); } else { fn(); } } else if (typeof data === 'object') { socket.json.send(data ? data : 'message_response'); // object or null } else { socket.send(data ? data : 'message_response'); // string or '' } }); socket.on('emit', function() { socket.emit('emit_response'); }); socket.on('emit_with_payload', function(payload) { socket.emit('emit_with_payload_response', payload); }); socket.on('emit_with_multiple_payloads', function(payload, payload) { socket.emit('emit_with_multiple_payloads_response', payload, payload); }); socket.on('emit_with_callback', function(fn) { fn(); }); socket.on('emit_with_callback_with_payload', function(fn) { fn(PAYLOAD); }); socket.on('emit_with_callback_with_multiple_payloads', function(fn) { fn(PAYLOAD, PAYLOAD); }); socket.on('emit_with_event', function(payload) { socket.emit('emit_with_event_response', payload); }); socket.on('ack', function(payload) { socket.emit('ack_response', payload, function(payload) { socket.emit('ack_callback_response', payload); }); }); socket.on('aaa', function() { socket.emit('aaa_response', PAYLOAD); }); socket.on('bbb', function(payload, fn) { if (fn) { fn(payload); } }); socket.on('wait_with_disconnect', function() { socket.emit('wait_with_disconnect_response'); }); }); var chat = io.of('/chat').on('connection', function (socket) { socket.on('emit_with_payload', function(payload) { socket.emit('emit_with_payload_response', payload); }); socket.on('aaa', function() { socket.emit('aaa_response', 'in chat'); }); socket.on('ack', function(payload) { socket.emit('ack_response', payload, function(payload) { socket.emit('ack_callback_response', payload); }); }); }); var news = io.of('/news').on('connection', function (socket) { socket.on('emit_with_payload', function(payload) { socket.emit('emit_with_payload_response', payload); }); socket.on('aaa', function() { socket.emit('aaa_response', 'in news'); }); }); var PAYLOAD = {'xxx': 'yyy'}; socketIO-client-0.5.3/setup.cfg000066400000000000000000000001431224316025000163250ustar00rootroot00000000000000[nosetests] detailed-errors=TRUE with-coverage=TRUE cover-package=socketIO_client cover-erase=TRUE socketIO-client-0.5.3/setup.py000066400000000000000000000015701224316025000162230ustar00rootroot00000000000000import os from setuptools import setup, find_packages here = os.path.abspath(os.path.dirname(__file__)) README = open(os.path.join(here, 'README.rst')).read() CHANGES = open(os.path.join(here, 'CHANGES.rst')).read() setup( name='socketIO-client', version='0.5.3', description='A socket.io client library', long_description=README + '\n\n' + CHANGES, license='MIT', classifiers=[ 'Intended Audience :: Developers', 'Programming Language :: Python', 'License :: OSI Approved :: MIT License', ], keywords='socket.io node.js', author='Roy Hyunjin Han', author_email='rhh@crosscompute.com', url='https://github.com/invisibleroads/socketIO-client', install_requires=[ 'requests', 'six', 'websocket-client', ], packages=find_packages(), include_package_data=True, zip_safe=True) socketIO-client-0.5.3/socketIO_client/000077500000000000000000000000001224316025000175645ustar00rootroot00000000000000socketIO-client-0.5.3/socketIO_client/__init__.py000066400000000000000000000333531224316025000217040ustar00rootroot00000000000000import logging import json import requests import time from collections import namedtuple from urlparse import urlparse from .exceptions import ConnectionError, TimeoutError, PacketError from .transports import _get_response, _negotiate_transport, TRANSPORTS _SocketIOSession = namedtuple('_SocketIOSession', [ 'id', 'heartbeat_timeout', 'server_supported_transports', ]) _log = logging.getLogger(__name__) PROTOCOL_VERSION = 1 RETRY_INTERVAL_IN_SECONDS = 1 class BaseNamespace(object): 'Define client behavior' def __init__(self, _transport, path): self._transport = _transport self.path = path self._callback_by_event = {} self.initialize() def initialize(self): 'Initialize custom variables here; you can override this method' pass def message(self, data='', callback=None): self._transport.message(self.path, data, callback) def emit(self, event, *args, **kw): callback, args = find_callback(args, kw) self._transport.emit(self.path, event, args, callback) def disconnect(self): self._transport.disconnect(self.path) def on(self, event, callback): 'Define a callback to handle a custom event emitted by the server' self._callback_by_event[event] = callback def on_connect(self): 'Called after server connects; you can override this method' _log.debug('%s [connect]', self.path) def on_disconnect(self): 'Called after server disconnects; you can override this method' _log.debug('%s [disconnect]', self.path) def on_heartbeat(self): 'Called after server sends a heartbeat; you can override this method' _log.debug('%s [heartbeat]', self.path) def on_message(self, data): 'Called after server sends a message; you can override this method' _log.info('%s [message] %s', self.path, data) def on_event(self, event, *args): """ Called after server sends an event; you can override this method. Called only if a custom event handler does not exist, such as one defined by namespace.on('my_event', my_function). """ callback, args = find_callback(args) arguments = [repr(_) for _ in args] if callback: arguments.append('callback(*args)') callback(*args) _log.info('%s [event] %s(%s)', self.path, event, ', '.join(arguments)) def on_error(self, reason, advice): 'Called after server sends an error; you can override this method' _log.info('%s [error] %s', self.path, advice) def on_noop(self): 'Called after server sends a noop; you can override this method' _log.info('%s [noop]', self.path) def on_open(self, *args): _log.info('%s [open] %s', self.path, args) def on_close(self, *args): _log.info('%s [close] %s', self.path, args) def on_retry(self, *args): _log.info('%s [retry] %s', self.path, args) def on_reconnect(self, *args): _log.info('%s [reconnect] %s', self.path, args) def _find_event_callback(self, event): # Check callbacks defined by on() try: return self._callback_by_event[event] except KeyError: pass # Check callbacks defined explicitly or use on_event() return getattr( self, 'on_' + event.replace(' ', '_'), lambda *args: self.on_event(event, *args)) class SocketIO(object): """Create a socket.io client that connects to a socket.io server at the specified host and port. - Define the behavior of the client by specifying a custom Namespace. - Prefix host with https:// to use SSL. - Set wait_for_connection=True to block until we have a connection. - Specify the transports you want to use. - Pass query params, headers, cookies, proxies as keyword arguments. SocketIO('localhost', 8000, params={'q': 'qqq'}, headers={'Authorization': 'Basic ' + b64encode('username:password')}, cookies={'a': 'aaa'}, proxies={'https': 'https://proxy.example.com:8080'}) """ def __init__( self, host, port=None, Namespace=BaseNamespace, wait_for_connection=True, transports=TRANSPORTS, **kw): self.is_secure, self.base_url = _parse_host(host, port) self.wait_for_connection = wait_for_connection self._namespace_by_path = {} self.client_supported_transports = transports self.kw = kw self.define(Namespace) def __enter__(self): return self def __exit__(self, *exception_pack): self.disconnect() def __del__(self): self.disconnect() def define(self, Namespace, path=''): if path: self._transport.connect(path) namespace = Namespace(self._transport, path) self._namespace_by_path[path] = namespace return namespace def on(self, event, callback, path=''): return self.get_namespace(path).on(event, callback) def message(self, data='', callback=None, path=''): self._transport.message(path, data, callback) def emit(self, event, *args, **kw): path = kw.get('path', '') callback, args = find_callback(args, kw) self._transport.emit(path, event, args, callback) def wait(self, seconds=None, for_callbacks=False): """Wait in a loop and process events as defined in the namespaces. - Omit seconds, i.e. call wait() without arguments, to wait forever. """ try: warning_screen = _yield_warning_screen(seconds) for elapsed_time in warning_screen: try: try: self._process_events() except TimeoutError: pass if self._stop_waiting(for_callbacks): break self.heartbeat_pacemaker.send(elapsed_time) except ConnectionError as e: try: warning = Exception('[connection error] %s' % e) warning_screen.throw(warning) except StopIteration: _log.warn(warning) self.disconnect() except KeyboardInterrupt: pass def _process_events(self): for packet in self._transport.recv_packet(): try: self._process_packet(packet) except PacketError as e: _log.warn('[packet error] %s', e) def _process_packet(self, packet): code, packet_id, path, data = packet namespace = self.get_namespace(path) delegate = self._get_delegate(code) delegate(packet, namespace._find_event_callback) def _stop_waiting(self, for_callbacks): # Use __transport to make sure that we do not reconnect inadvertently if for_callbacks and not self.__transport.has_ack_callback: return True if self.__transport._wants_to_disconnect: return True return False def wait_for_callbacks(self, seconds=None): self.wait(seconds, for_callbacks=True) def disconnect(self, path=''): if self.connected: self._transport.disconnect(path) namespace = self._namespace_by_path[path] namespace.on_disconnect() if path: del self._namespace_by_path[path] @property def connected(self): return self.__transport.connected @property def _transport(self): try: if self.connected: return self.__transport except AttributeError: pass warning_screen = _yield_warning_screen(seconds=None) for elapsed_time in warning_screen: try: self.__transport = self._get_transport() break except ConnectionError as e: if not self.wait_for_connection: raise try: warning = Exception('[waiting for connection] %s' % e) warning_screen.throw(warning) except StopIteration: _log.warn(warning) return self.__transport def _get_transport(self): socketIO_session = _get_socketIO_session( self.is_secure, self.base_url, **self.kw) _log.debug('[transports available] %s', ' '.join( socketIO_session.server_supported_transports)) # Initialize heartbeat_pacemaker self.heartbeat_pacemaker = self._make_heartbeat_pacemaker( heartbeat_interval=socketIO_session.heartbeat_timeout / 2) self.heartbeat_pacemaker.next() # Negotiate transport transport = _negotiate_transport( self.client_supported_transports, socketIO_session, self.is_secure, self.base_url, **self.kw) # Update namespaces for namespace in self._namespace_by_path.values(): namespace._transport = transport return transport def _make_heartbeat_pacemaker(self, heartbeat_interval): heartbeat_time = 0 while True: elapsed_time = (yield) if elapsed_time - heartbeat_time > heartbeat_interval: heartbeat_time = elapsed_time self._transport.send_heartbeat() def get_namespace(self, path=''): try: return self._namespace_by_path[path] except KeyError: raise PacketError('unexpected namespace path (%s)' % path) def _get_delegate(self, code): try: return { '0': self._on_disconnect, '1': self._on_connect, '2': self._on_heartbeat, '3': self._on_message, '4': self._on_json, '5': self._on_event, '6': self._on_ack, '7': self._on_error, '8': self._on_noop, }[code] except KeyError: raise PacketError('unexpected code (%s)' % code) def _on_disconnect(self, packet, find_event_callback): find_event_callback('disconnect')() def _on_connect(self, packet, find_event_callback): find_event_callback('connect')() def _on_heartbeat(self, packet, find_event_callback): find_event_callback('heartbeat')() def _on_message(self, packet, find_event_callback): code, packet_id, path, data = packet args = [data] if packet_id: args.append(self._prepare_to_send_ack(path, packet_id)) find_event_callback('message')(*args) def _on_json(self, packet, find_event_callback): code, packet_id, path, data = packet args = [json.loads(data)] if packet_id: args.append(self._prepare_to_send_ack(path, packet_id)) find_event_callback('message')(*args) def _on_event(self, packet, find_event_callback): code, packet_id, path, data = packet value_by_name = json.loads(data) event = value_by_name['name'] args = value_by_name.get('args', []) if packet_id: args.append(self._prepare_to_send_ack(path, packet_id)) find_event_callback(event)(*args) def _on_ack(self, packet, find_event_callback): code, packet_id, path, data = packet data_parts = data.split('+', 1) packet_id = data_parts[0] try: ack_callback = self._transport.get_ack_callback(packet_id) except KeyError: return args = json.loads(data_parts[1]) if len(data_parts) > 1 else [] ack_callback(*args) def _on_error(self, packet, find_event_callback): code, packet_id, path, data = packet reason, advice = data.split('+', 1) find_event_callback('error')(reason, advice) def _on_noop(self, packet, find_event_callback): find_event_callback('noop')() def _prepare_to_send_ack(self, path, packet_id): 'Return function that acknowledges the server' return lambda *args: self._transport.ack(path, packet_id, *args) def find_callback(args, kw=None): 'Return callback whether passed as a last argument or as a keyword' if args and callable(args[-1]): return args[-1], args[:-1] try: return kw['callback'], args except (KeyError, TypeError): return None, args def _parse_host(host, port): if not host.startswith('http'): host = 'http://' + host url_pack = urlparse(host) is_secure = url_pack.scheme == 'https' port = port or url_pack.port or (443 if is_secure else 80) base_url = '%s:%d%s/socket.io/%s' % ( url_pack.hostname, port, url_pack.path, PROTOCOL_VERSION) return is_secure, base_url def _yield_warning_screen(seconds=None): last_warning = None for elapsed_time in _yield_elapsed_time(seconds): try: yield elapsed_time except Exception as warning: warning = str(warning) if last_warning != warning: last_warning = warning _log.warn(warning) time.sleep(RETRY_INTERVAL_IN_SECONDS) def _yield_elapsed_time(seconds=None): start_time = time.time() if seconds is None: while True: yield time.time() - start_time while time.time() - start_time < seconds: yield time.time() - start_time def _get_socketIO_session(is_secure, base_url, **kw): server_url = '%s://%s/' % ('https' if is_secure else 'http', base_url) try: response = _get_response(requests.get, server_url, **kw) except TimeoutError as e: raise ConnectionError(e) response_parts = response.text.split(':') return _SocketIOSession( id=response_parts[0], heartbeat_timeout=int(response_parts[1]), server_supported_transports=response_parts[3].split(',')) socketIO-client-0.5.3/socketIO_client/exceptions.py000066400000000000000000000002651224316025000223220ustar00rootroot00000000000000class SocketIOError(Exception): pass class ConnectionError(SocketIOError): pass class TimeoutError(SocketIOError): pass class PacketError(SocketIOError): pass socketIO-client-0.5.3/socketIO_client/tests.py000066400000000000000000000172371224316025000213120ustar00rootroot00000000000000import logging import time from unittest import TestCase from . import SocketIO, BaseNamespace, find_callback from .transports import TIMEOUT_IN_SECONDS HOST = 'localhost' PORT = 8000 DATA = 'xxx' PAYLOAD = {'xxx': 'yyy'} logging.basicConfig(level=logging.DEBUG) class BaseMixin(object): def setUp(self): self.called_on_response = False def tearDown(self): del self.socketIO def on_response(self, *args): for arg in args: if isinstance(arg, dict): self.assertEqual(arg, PAYLOAD) else: self.assertEqual(arg, DATA) self.called_on_response = True def test_disconnect(self): 'Disconnect' self.assertTrue(self.socketIO.connected) self.socketIO.disconnect() self.assertFalse(self.socketIO.connected) # Use context manager with SocketIO(HOST, PORT, Namespace) as self.socketIO: namespace = self.socketIO.get_namespace() self.assertFalse(namespace.called_on_disconnect) self.assertTrue(self.socketIO.connected) self.assertTrue(namespace.called_on_disconnect) self.assertFalse(self.socketIO.connected) def test_message(self): 'Message' namespace = self.socketIO.define(Namespace) self.socketIO.message() self.socketIO.wait(self.wait_time_in_seconds) self.assertEqual(namespace.response, 'message_response') def test_message_with_data(self): 'Message with data' namespace = self.socketIO.define(Namespace) self.socketIO.message(DATA) self.socketIO.wait(self.wait_time_in_seconds) self.assertEqual(namespace.response, DATA) def test_message_with_payload(self): 'Message with payload' namespace = self.socketIO.define(Namespace) self.socketIO.message(PAYLOAD) self.socketIO.wait(self.wait_time_in_seconds) self.assertEqual(namespace.response, PAYLOAD) def test_message_with_callback(self): 'Message with callback' self.socketIO.message(callback=self.on_response) self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds) self.assertTrue(self.called_on_response) def test_message_with_callback_with_data(self): 'Message with callback with data' self.socketIO.message(DATA, self.on_response) self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds) self.assertTrue(self.called_on_response) def test_emit(self): 'Emit' namespace = self.socketIO.define(Namespace) self.socketIO.emit('emit') self.socketIO.wait(self.wait_time_in_seconds) self.assertEqual(namespace.args_by_event, { 'emit_response': (), }) def test_emit_with_payload(self): 'Emit with payload' namespace = self.socketIO.define(Namespace) self.socketIO.emit('emit_with_payload', PAYLOAD) self.socketIO.wait(self.wait_time_in_seconds) self.assertEqual(namespace.args_by_event, { 'emit_with_payload_response': (PAYLOAD,), }) def test_emit_with_multiple_payloads(self): 'Emit with multiple payloads' namespace = self.socketIO.define(Namespace) self.socketIO.emit('emit_with_multiple_payloads', PAYLOAD, PAYLOAD) self.socketIO.wait(self.wait_time_in_seconds) self.assertEqual(namespace.args_by_event, { 'emit_with_multiple_payloads_response': (PAYLOAD, PAYLOAD), }) def test_emit_with_callback(self): 'Emit with callback' self.socketIO.emit('emit_with_callback', self.on_response) self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds) self.assertTrue(self.called_on_response) def test_emit_with_callback_with_payload(self): 'Emit with callback with payload' self.socketIO.emit( 'emit_with_callback_with_payload', self.on_response) self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds) self.assertTrue(self.called_on_response) def test_emit_with_callback_with_multiple_payloads(self): 'Emit with callback with multiple payloads' self.socketIO.emit( 'emit_with_callback_with_multiple_payloads', self.on_response) self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds) self.assertTrue(self.called_on_response) def test_emit_with_event(self): 'Emit to trigger an event' self.socketIO.on('emit_with_event_response', self.on_response) self.socketIO.emit('emit_with_event', PAYLOAD) self.socketIO.wait(self.wait_time_in_seconds) self.assertTrue(self.called_on_response) def test_ack(self): 'Trigger server callback' namespace = self.socketIO.define(Namespace) self.socketIO.emit('ack', PAYLOAD) self.socketIO.wait(self.wait_time_in_seconds) self.assertEqual(namespace.args_by_event, { 'ack_response': (PAYLOAD,), 'ack_callback_response': (PAYLOAD,), }) def test_wait_with_disconnect(self): 'Exit loop when the client wants to disconnect' self.socketIO.define(Namespace) self.socketIO.emit('wait_with_disconnect') timeout_in_seconds = 5 start_time = time.time() self.socketIO.wait(timeout_in_seconds) self.assertTrue(time.time() - start_time < timeout_in_seconds) def test_namespace_emit(self): 'Behave differently in different namespaces' main_namespace = self.socketIO.define(Namespace) chat_namespace = self.socketIO.define(Namespace, '/chat') news_namespace = self.socketIO.define(Namespace, '/news') news_namespace.emit('emit_with_payload', PAYLOAD) self.socketIO.wait(self.wait_time_in_seconds) self.assertEqual(main_namespace.args_by_event, {}) self.assertEqual(chat_namespace.args_by_event, {}) self.assertEqual(news_namespace.args_by_event, { 'emit_with_payload_response': (PAYLOAD,), }) def test_namespace_ack(self): 'Trigger server callback' chat_namespace = self.socketIO.define(Namespace, '/chat') chat_namespace.emit('ack', PAYLOAD) self.socketIO.wait(self.wait_time_in_seconds) self.assertEqual(chat_namespace.args_by_event, { 'ack_response': (PAYLOAD,), 'ack_callback_response': (PAYLOAD,), }) class Test_WebsocketTransport(TestCase, BaseMixin): def setUp(self): super(Test_WebsocketTransport, self).setUp() self.socketIO = SocketIO(HOST, PORT, transports=['websocket']) self.wait_time_in_seconds = 0.1 class Test_XHR_PollingTransport(TestCase, BaseMixin): def setUp(self): super(Test_XHR_PollingTransport, self).setUp() self.socketIO = SocketIO(HOST, PORT, transports=['xhr-polling']) self.wait_time_in_seconds = TIMEOUT_IN_SECONDS + 1 class Test_JSONP_PollingTransport(TestCase, BaseMixin): def setUp(self): super(Test_JSONP_PollingTransport, self).setUp() self.socketIO = SocketIO(HOST, PORT, transports=['jsonp-polling']) self.wait_time_in_seconds = TIMEOUT_IN_SECONDS + 1 class Namespace(BaseNamespace): def initialize(self): self.response = None self.args_by_event = {} self.called_on_disconnect = False def on_disconnect(self): self.called_on_disconnect = True def on_message(self, data): self.response = data def on_event(self, event, *args): callback, args = find_callback(args) if callback: callback(*args) self.args_by_event[event] = args def on_wait_with_disconnect_response(self): self.disconnect() socketIO-client-0.5.3/socketIO_client/transports.py000066400000000000000000000256541224316025000223710ustar00rootroot00000000000000import json import logging import re import requests import six import socket import time import websocket from itertools import izip from .exceptions import SocketIOError, ConnectionError, TimeoutError TRANSPORTS = 'websocket', 'xhr-polling', 'jsonp-polling' BOUNDARY = six.u('\ufffd') TIMEOUT_IN_SECONDS = 3 _log = logging.getLogger(__name__) class _AbstractTransport(object): def __init__(self): self._packet_id = 0 self._callback_by_packet_id = {} self._wants_to_disconnect = False self._packets = [] def disconnect(self, path=''): if not path: self._wants_to_disconnect = True if not self.connected: return if path: self.send_packet(0, path) else: self.close() def connect(self, path): self.send_packet(1, path) def send_heartbeat(self): self.send_packet(2) def message(self, path, data, callback): if isinstance(data, basestring): code = 3 else: code = 4 data = json.dumps(data, ensure_ascii=False) self.send_packet(code, path, data, callback) def emit(self, path, event, args, callback): data = json.dumps(dict(name=event, args=args), ensure_ascii=False) self.send_packet(5, path, data, callback) def ack(self, path, packet_id, *args): packet_id = packet_id.rstrip('+') data = '%s+%s' % ( packet_id, json.dumps(args, ensure_ascii=False), ) if args else packet_id self.send_packet(6, path, data) def noop(self, path=''): self.send_packet(8, path) def send_packet(self, code, path='', data='', callback=None): packet_id = self.set_ack_callback(callback) if callback else '' packet_parts = str(code), packet_id, path, data packet_text = ':'.join(packet_parts) self.send(packet_text) _log.debug('[packet sent] %s', packet_text) def recv_packet(self): try: while self._packets: yield self._packets.pop(0) except IndexError: pass for packet_text in self.recv(): _log.debug('[packet received] %s', packet_text) try: packet_parts = packet_text.split(':', 3) except AttributeError: _log.warn('[packet error] %s', packet_text) continue code, packet_id, path, data = None, None, None, None packet_count = len(packet_parts) if 4 == packet_count: code, packet_id, path, data = packet_parts elif 3 == packet_count: code, packet_id, path = packet_parts elif 1 == packet_count: code = packet_parts[0] yield code, packet_id, path, data def _enqueue_packet(self, packet): self._packets.append(packet) def set_ack_callback(self, callback): 'Set callback to be called after server sends an acknowledgment' self._packet_id += 1 self._callback_by_packet_id[str(self._packet_id)] = callback return '%s+' % self._packet_id def get_ack_callback(self, packet_id): 'Get callback to be called after server sends an acknowledgment' callback = self._callback_by_packet_id[packet_id] del self._callback_by_packet_id[packet_id] return callback @property def has_ack_callback(self): return True if self._callback_by_packet_id else False class _WebsocketTransport(_AbstractTransport): def __init__(self, socketIO_session, is_secure, base_url, **kw): super(_WebsocketTransport, self).__init__() url = '%s://%s/websocket/%s' % ( 'wss' if is_secure else 'ws', base_url, socketIO_session.id) try: self._connection = websocket.create_connection(url) except socket.timeout as e: raise ConnectionError(e) except socket.error as e: raise ConnectionError(e) self._connection.settimeout(TIMEOUT_IN_SECONDS) @property def connected(self): return self._connection.connected def send(self, packet_text): try: self._connection.send(packet_text) except websocket.WebSocketTimeoutException as e: message = 'timed out while sending %s (%s)' % (packet_text, e) _log.warn(message) raise TimeoutError(e) except socket.error as e: message = 'disconnected while sending %s (%s)' % (packet_text, e) _log.warn(message) raise ConnectionError(message) def recv(self): try: yield self._connection.recv() except websocket.WebSocketTimeoutException as e: raise TimeoutError(e) except websocket.SSLError as e: raise ConnectionError(e) except websocket.WebSocketConnectionClosedException as e: raise ConnectionError('connection closed (%s)' % e) except socket.error as e: raise ConnectionError(e) def close(self): self._connection.close() class _XHR_PollingTransport(_AbstractTransport): def __init__(self, socketIO_session, is_secure, base_url, **kw): super(_XHR_PollingTransport, self).__init__() self._url = '%s://%s/xhr-polling/%s' % ( 'https' if is_secure else 'http', base_url, socketIO_session.id) self._connected = True self._http_session = _prepare_http_session(kw) # Create connection for packet in self.recv_packet(): self._enqueue_packet(packet) @property def connected(self): return self._connected @property def _params(self): return dict(t=int(time.time())) def send(self, packet_text): _get_response( self._http_session.post, self._url, params=self._params, data=packet_text, timeout=TIMEOUT_IN_SECONDS) def recv(self): response = _get_response( self._http_session.get, self._url, params=self._params, timeout=TIMEOUT_IN_SECONDS) response_text = response.text if not response_text.startswith(BOUNDARY): yield response_text return for packet_text in _yield_text_from_framed_data(response_text): yield packet_text def close(self): _get_response( self._http_session.get, self._url, params=dict(self._params.items() + [('disconnect', True)])) self._connected = False class _JSONP_PollingTransport(_AbstractTransport): RESPONSE_PATTERN = re.compile(r'io.j\[(\d+)\]\("(.*)"\);') def __init__(self, socketIO_session, is_secure, base_url, **kw): super(_JSONP_PollingTransport, self).__init__() self._url = '%s://%s/jsonp-polling/%s' % ( 'https' if is_secure else 'http', base_url, socketIO_session.id) self._connected = True self._http_session = _prepare_http_session(kw) self._id = 0 # Create connection for packet in self.recv_packet(): self._enqueue_packet(packet) @property def connected(self): return self._connected @property def _params(self): return dict(t=int(time.time()), i=self._id) def send(self, packet_text): _get_response( self._http_session.post, self._url, params=self._params, data='d=%s' % requests.utils.quote(json.dumps(packet_text)), headers={'content-type': 'application/x-www-form-urlencoded'}, timeout=TIMEOUT_IN_SECONDS) def recv(self): 'Decode the JavaScript response so that we can parse it as JSON' response = _get_response( self._http_session.get, self._url, params=self._params, headers={'content-type': 'text/javascript; charset=UTF-8'}, timeout=TIMEOUT_IN_SECONDS) response_text = response.text try: self._id, response_text = self.RESPONSE_PATTERN.match( response_text).groups() except AttributeError: _log.warn('[packet error] %s', response_text) return if not response_text.startswith(BOUNDARY): yield response_text.decode('unicode_escape') return for packet_text in _yield_text_from_framed_data( response_text, parse=lambda x: x.decode('unicode_escape')): yield packet_text def close(self): _get_response( self._http_session.get, self._url, params=dict(self._params.items() + [('disconnect', True)])) self._connected = False def _negotiate_transport( client_supported_transports, session, is_secure, base_url, **kw): server_supported_transports = session.server_supported_transports for supported_transport in client_supported_transports: if supported_transport in server_supported_transports: _log.debug('[transport selected] %s', supported_transport) return { 'websocket': _WebsocketTransport, 'xhr-polling': _XHR_PollingTransport, 'jsonp-polling': _JSONP_PollingTransport, }[supported_transport](session, is_secure, base_url, **kw) raise SocketIOError(' '.join([ 'could not negotiate a transport:', 'client supports %s but' % ', '.join(client_supported_transports), 'server supports %s' % ', '.join(server_supported_transports), ])) def _yield_text_from_framed_data(framed_data, parse=lambda x: x): parts = [parse(x) for x in framed_data.split(BOUNDARY)] for text_length, text in izip(parts[1::2], parts[2::2]): if text_length != str(len(text)): warning = 'invalid declared length=%s for packet_text=%s' % ( text_length, text) _log.warn('[packet error] %s', warning) continue yield text def _get_response(request, *args, **kw): try: response = request(*args, **kw) except requests.exceptions.Timeout as e: raise TimeoutError(e) except requests.exceptions.ConnectionError as e: raise ConnectionError(e) except requests.exceptions.SSLError as e: raise ConnectionError('could not negotiate SSL (%s)' % e) status = response.status_code if 200 != status: raise ConnectionError('unexpected status code (%s)' % status) return response def _prepare_http_session(kw): http_session = requests.Session() http_session.headers.update(kw.get('headers', {})) http_session.auth = kw.get('auth') http_session.proxies.update(kw.get('proxies', {})) http_session.hooks.update(kw.get('hooks', {})) http_session.params.update(kw.get('params', {})) http_session.verify = kw.get('verify') http_session.cert = kw.get('cert') http_session.cookies.update(kw.get('cookies', {})) return http_session