diff --git a/CHANGES.rst b/CHANGES.rst index 9fcbea8..b4dbbab 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,7 @@ +0.5.6 +----- +- Backported to support requests 0.8.2 + 0.5.5 ----- - Fixed reconnection in the event of server restart diff --git a/README.rst b/README.rst index 7bc0f48..b864c88 100644 --- a/README.rst +++ b/README.rst @@ -6,6 +6,8 @@ socketIO-client =============== Here is a `socket.io `_ client library for Python. You can use it to write test code for your socket.io server. +Please note that this version implements `socket.io protocol 0.9 `_, which is compatible with `gevent-socketio `_. If you want to communicate using `socket.io protocol 1.x `_, please use `socketIO-client 0.6.3 `_ or higher. + Installation ------------ @@ -32,6 +34,7 @@ Activate isolated environment. :: Launch your socket.io server. :: + npm install -g socket.io@0.9 node serve-tests.js For debugging information, run these commands first. :: @@ -156,5 +159,5 @@ Credits - `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 `_, `Patrick Huber `_, `Brad Campbell `_, `Daniel `_, `Sean Arietta `_ submitted code to expand support of the socket.io protocol. - `Bernard Pratz `_, `Francis Bull `_ wrote prototypes to support xhr-polling and jsonp-polling. -- `Eric Chen `_, `Denis Zinevich `_, `Thiago Hersan `_, `Nayef Copty `_, `Jörgen Karlsson `_, `Branden Ghena `_ suggested ways to make the connection more robust. +- `Eric Chen `_, `Denis Zinevich `_, `Thiago Hersan `_, `Nayef Copty `_, `Jörgen Karlsson `_, `Branden Ghena `_, `Tim Landscheidt `_ suggested ways to make the connection more robust. - `Merlijn van Deen `_, `Frederic Sureau `_, `Marcus Cobden `_, `Drew Hutchison `_, `wuurrd `_, `Adam Kecer `_, `Alex Monk `_, `Vishal P R `_, `John Vandenberg `_, `Thomas Grainger `_ proposed changes that make the library more friendly and practical for you! diff --git a/TODO.goals b/TODO.goals index a3b2527..e69de29 100644 --- a/TODO.goals +++ b/TODO.goals @@ -1,8 +0,0 @@ -Release 0.6.1 #41 #52 - + Update serve-tests.js - + Integrate serve-tests.js changes from sarietta - = Put tests in index.html - Update tests - Merge sarietta's pull request - Revive heartbeat as separate process -Implement rooms #65 diff --git a/setup.py b/setup.py index b98da09..7a7567b 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ CHANGES = open(os.path.join(here, 'CHANGES.rst')).read() setup( name='socketIO-client', - version='0.5.5', + version='0.5.6', description='A socket.io client library', long_description=README + '\n\n' + CHANGES, license='MIT', diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index f06b5cb..208e139 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -10,6 +10,7 @@ except ImportError: from .exceptions import ( SocketIOError, ConnectionError, TimeoutError, PacketError) +from .symmetries import _get_text from .transports import ( _get_response, TRANSPORTS, _WebsocketTransport, _XHR_PollingTransport, _JSONP_PollingTransport) @@ -78,7 +79,7 @@ class BaseNamespace(object): """ callback, args = find_callback(args) if callback: - callback(*args) + callback(args) def on_error(self, reason, advice): 'Called after server sends an error; you can override this method' @@ -116,7 +117,7 @@ class BaseNamespace(object): return getattr( self, 'on_' + event.replace(' ', '_'), - lambda *args: self.on_event(event, *args)) + lambda *args: self.on_event(event, args)) class LoggingNamespace(BaseNamespace): @@ -147,7 +148,7 @@ class LoggingNamespace(BaseNamespace): arguments.append('callback(*args)') self._log(logging.INFO, '%s [event] %s(%s)', self.path, event, ', '.join(arguments)) - super(LoggingNamespace, self).on_event(event, *args) + super(LoggingNamespace, self).on_event(event, args) def on_error(self, reason, advice): self._log(logging.INFO, '%s [error] %s', self.path, advice) @@ -236,7 +237,7 @@ class SocketIO(object): callback, args = find_callback(args, kw) self._transport.emit(path, event, args, callback) - def wait(self, seconds=None, for_callbacks=False): + def wait(self, seconds=None, for_callbacks=False, on_error=None, error_args=None): """Wait in a loop and process events as defined in the namespaces. - Omit seconds, i.e. call wait() without arguments, to wait forever. @@ -253,6 +254,8 @@ class SocketIO(object): pass next(self._heartbeat_pacemaker) except ConnectionError as e: + if on_error is not None and callable(on_error): + on_error(e, error_args) try: warning = Exception('[connection error] %s' % e) warning_screen.throw(warning) @@ -273,8 +276,8 @@ class SocketIO(object): def _process_packet(self, packet): code, packet_id, path, data = packet - namespace = self.get_namespace(path) - delegate = self._get_delegate(code) + namespace = self.get_namespace(path or '') + delegate = self._get_delegate(code, packet) delegate(packet, namespace._find_event_callback) def _stop_waiting(self, for_callbacks): @@ -314,7 +317,7 @@ class SocketIO(object): try: if self.connected: return self.__transport - except AttributeError: + except AttributeError as e: pass socketIO_session = self._get_socketIO_session() supported_transports = self._get_supported_transports(socketIO_session) @@ -394,7 +397,7 @@ class SocketIO(object): except KeyError: raise PacketError('unhandled namespace path (%s)' % path) - def _get_delegate(self, code): + def _get_delegate(self, code, packet): try: return { '0': self._on_disconnect, @@ -406,9 +409,10 @@ class SocketIO(object): '6': self._on_ack, '7': self._on_error, '8': self._on_noop, + '': self._on_noop }[code] except KeyError: - raise PacketError('unexpected code (%s)' % code) + raise PacketError('unexpected code ({}): {}'.format([code], packet)) def _on_disconnect(self, packet, find_event_callback): find_event_callback('disconnect')() @@ -421,26 +425,27 @@ class SocketIO(object): def _on_message(self, packet, find_event_callback): code, packet_id, path, data = packet - args = [data] + args = data + self._send_args('message', args, path, packet_id, find_event_callback) + + def _send_args(self, event, args, path, packet_id, find_event_callback): + ev_args = [args] if packet_id: - args.append(self._prepare_to_send_ack(path, packet_id)) - find_event_callback('message')(*args) + ack = self._prepare_to_send_ack(path, packet_id) + ev_args.append(ack) + find_event_callback(event)(*ev_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) + args = json.loads(data) + self._send_args('message', args, path, packet_id, find_event_callback) 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) + self._send_args(event, args, path, packet_id, find_event_callback) def _on_ack(self, packet, find_event_callback): code, packet_id, path, data = packet @@ -451,7 +456,7 @@ class SocketIO(object): except KeyError: return args = json.loads(data_parts[1]) if len(data_parts) > 1 else [] - ack_callback(*args) + ack_callback(args) def _on_error(self, packet, find_event_callback): code, packet_id, path, data = packet @@ -515,7 +520,7 @@ def _get_socketIO_session(is_secure, base_url, **kw): response = _get_response(requests.get, server_url, **kw) except TimeoutError as e: raise ConnectionError(e) - response_parts = response.text.split(':') + response_parts = _get_text(response).split(':') return _SocketIOSession( id=response_parts[0], heartbeat_timeout=int(response_parts[1]), diff --git a/socketIO_client/symmetries.py b/socketIO_client/symmetries.py new file mode 100644 index 0000000..eceecdc --- /dev/null +++ b/socketIO_client/symmetries.py @@ -0,0 +1,5 @@ +def _get_text(response): + try: + return response.text # requests 2.7.0 + except AttributeError: + return response.content # requests 0.8.2 diff --git a/socketIO_client/transports.py b/socketIO_client/transports.py index 2887efa..e479cd1 100644 --- a/socketIO_client/transports.py +++ b/socketIO_client/transports.py @@ -8,8 +8,10 @@ import socket import sys import time import websocket +import re from .exceptions import ConnectionError, TimeoutError +from .symmetries import _get_text if not hasattr(websocket, 'create_connection'): @@ -84,6 +86,8 @@ class _AbstractTransport(object): 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, encode_unicode(data) + if not data: + packet_parts = packet_parts[:-1] packet_text = ':'.join(packet_parts) self.send(packet_text) self._log(logging.DEBUG, '[packet sent] %s', packet_text) @@ -94,22 +98,34 @@ class _AbstractTransport(object): yield self._packets.pop(0) except IndexError: pass - for packet_text in self.recv(timeout=timeout): - self._log(logging.DEBUG, '[packet received] %s', packet_text) - try: - packet_parts = packet_text.split(':', 3) - except AttributeError: - self._log(logging.WARNING, '[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 + for packet_texts in self.recv(timeout=timeout): + #remove packet separator + packet_texts = re.sub('^\xef\xbf\xbd\w+\xef\xbf\xbd', '', + packet_texts) + + packets = packet_texts.split('\xef\xbf\xbd')[::2] + + for packet_text in packets: + self._log(logging.DEBUG, '[packet received] %s', packet_text) + sep_count = packet_text.count('\xef\xbf\xbd')/2 + + try: + packet_parts = packet_text.split(':', 3) + except AttributeError: + self._log(logging.WARNING, '[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] + if code and len(code) > 1: + code = code[-1] + + yield code, packet_id, path, data def _enqueue_packet(self, packet): self._packets.append(packet) @@ -142,6 +158,8 @@ class _WebsocketTransport(_AbstractTransport): http_session = _prepare_http_session(kw) req = http_session.prepare_request(requests.Request('GET', url)) headers = ['%s: %s' % item for item in req.headers.items()] + headers.append('Connection: keep-alive') + try: self._connection = websocket.create_connection(url, header=headers) except socket.timeout as e: @@ -223,7 +241,7 @@ class _XHR_PollingTransport(_AbstractTransport): params=self._params, timeout=timeout or TIMEOUT_IN_SECONDS, stream=True) - response_text = response.text + response_text = _get_text(response) if not response_text.startswith(BOUNDARY): yield response_text return @@ -279,7 +297,7 @@ class _JSONP_PollingTransport(_AbstractTransport): params=self._params, headers={'content-type': 'text/javascript; charset=UTF-8'}, timeout=timeout or TIMEOUT_IN_SECONDS) - response_text = response.text + response_text = _get_text(response) try: self._id, response_text = self.RESPONSE_PATTERN.match( response_text).groups() @@ -317,10 +335,10 @@ def _get_response(request, *args, **kw): 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) + except requests.exceptions.ConnectionError as e: + raise ConnectionError(e) status = response.status_code if 200 != status: raise ConnectionError('unexpected status code (%s)' % status)