Compare commits
226 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8aa519e095 | ||
|
|
d3a5839678 | ||
|
|
7e9f3c7343 | ||
|
|
4f04e6ca62 | ||
|
|
fd88b9e096 | ||
|
|
1cd63a031b | ||
|
|
db59b16e25 | ||
|
|
ba82ffe558 | ||
|
|
59dcd02a91 | ||
|
|
605194d515 | ||
|
|
689648ba32 | ||
|
|
29a3e0ebf6 | ||
|
|
aeaad0c330 | ||
|
|
12fa6c523b | ||
|
|
829e669403 | ||
|
|
fb3b66115e | ||
|
|
16b538d847 | ||
|
|
e622312a79 | ||
|
|
eb0324cb68 | ||
|
|
fd106e623d | ||
|
|
b3f61bb26d | ||
|
|
9e0bb069ad | ||
|
|
4b6c562825 | ||
|
|
a6f9260964 | ||
|
|
f5b157014d | ||
|
|
978a669d16 | ||
|
|
601ba59971 | ||
|
|
57da37e995 | ||
|
|
64fad75d4c | ||
|
|
55b88b51d5 | ||
|
|
82ef84bffd | ||
|
|
c0e40ba131 | ||
|
|
43c294f6e6 | ||
|
|
1f9d97fc33 | ||
|
|
af91855b18 | ||
|
|
444386f271 | ||
|
|
0d1a2b9ca2 | ||
|
|
8f9da7f4d0 | ||
|
|
922de4d160 | ||
|
|
5146b2f0bc | ||
|
|
34c56a3da3 | ||
|
|
f67efd7119 | ||
|
|
3d8efe0eb4 | ||
|
|
4fa6dcbe8d | ||
|
|
5806f23492 | ||
|
|
dec8e09327 | ||
|
|
9fe0c05926 | ||
|
|
ebd18b7017 | ||
|
|
5ecc5fb36c | ||
|
|
9afe5c2f2a | ||
|
|
96014d3d53 | ||
|
|
23844f9e76 | ||
|
|
8641818cf2 | ||
|
|
7e105e13b5 | ||
|
|
ef43467870 | ||
|
|
f34c7014fb | ||
|
|
8289624c47 | ||
|
|
b70cb7ffe5 | ||
|
|
911c04cbf1 | ||
|
|
38e6038b8f | ||
|
|
98c5ecea7f | ||
|
|
5917925ccc | ||
|
|
d96831de19 | ||
|
|
cd92f1c0cc | ||
|
|
24ce2dc6c0 | ||
|
|
082c4e21eb | ||
|
|
e38628d178 | ||
|
|
006d1ac133 | ||
|
|
a42704e0ab | ||
|
|
d09f808cd3 | ||
|
|
8dbfa58d7c | ||
|
|
d61163569a | ||
|
|
244c8c6f3f | ||
|
|
6627e360d7 | ||
|
|
97b3325d6f | ||
|
|
326b152b05 | ||
|
|
fbb1619ef6 | ||
|
|
2401a268a1 | ||
|
|
154874cfc9 | ||
|
|
502e54d7bb | ||
|
|
63a6fca613 | ||
|
|
f8a3d7cbe6 | ||
|
|
4dea8b080e | ||
|
|
66c11a0a37 | ||
|
|
d0ba033ac3 | ||
|
|
6b2f556471 | ||
|
|
526a4df25e | ||
|
|
901e6c5113 | ||
|
|
ddaf2ea44f | ||
|
|
e98f98f9a2 | ||
|
|
f7a5788bf7 | ||
|
|
0c6d4cd4d7 | ||
|
|
0c8dda1316 | ||
|
|
6cdf1eb610 | ||
|
|
3aab1f1436 | ||
|
|
c19f9891e6 | ||
|
|
47b5f2dd69 | ||
|
|
18cf130d9e | ||
|
|
e16669a824 | ||
|
|
b8f838b392 | ||
|
|
99b29ac841 | ||
|
|
5f7937dc1e | ||
|
|
4f7605c0b3 | ||
|
|
1eb8faa247 | ||
|
|
fc3a8b5e32 | ||
|
|
cfbaaa21eb | ||
|
|
8e23fc73a5 | ||
|
|
b9e63ff7cd | ||
|
|
c1e4924717 | ||
|
|
ee425459bd | ||
|
|
01f166db1e | ||
|
|
6661d8be0e | ||
|
|
315b795a91 | ||
|
|
698d083665 | ||
|
|
88bf4c0bcc | ||
|
|
b394c8a55a | ||
|
|
5f27c22681 | ||
|
|
cdfbe55203 | ||
|
|
d73d6145f3 | ||
|
|
2a43420e1b | ||
|
|
f44485d3e2 | ||
|
|
ef64649a2a | ||
|
|
5af21af575 | ||
|
|
b059cdf02c | ||
|
|
dc8e867741 | ||
|
|
f06bf8e736 | ||
|
|
a1f37e8e60 | ||
|
|
6932d4c2d1 | ||
|
|
455890a700 | ||
|
|
2361706c4e | ||
|
|
6f8e76adfd | ||
|
|
66e563acc3 | ||
|
|
bdb92f8da6 | ||
|
|
f4a96c72d0 | ||
|
|
2d1257bf8f | ||
|
|
5c1d38ac86 | ||
|
|
16c64437d3 | ||
|
|
07a5cc4c63 | ||
|
|
57971b5f71 | ||
|
|
d64e947aac | ||
|
|
63dea4f7a3 | ||
|
|
37bd6786d7 | ||
|
|
eda747ec4e | ||
|
|
cbdf009d46 | ||
|
|
a0b061cbdc | ||
|
|
9611d5d107 | ||
|
|
c3428e0b68 | ||
|
|
895d2447fd | ||
|
|
64d1c010a9 | ||
|
|
1c2a313002 | ||
|
|
7254172545 | ||
|
|
b0d67b98af | ||
|
|
e108c8a12a | ||
|
|
9a80988e26 | ||
|
|
557edb8620 | ||
|
|
9285f3da02 | ||
|
|
20b15872ee | ||
|
|
3d12aa8a28 | ||
|
|
c35316f158 | ||
|
|
f182df8580 | ||
|
|
6a5a35d84d | ||
|
|
c82a8301bb | ||
|
|
8f97e73bac | ||
|
|
e16652c4cc | ||
|
|
6bef8c0009 | ||
|
|
fa454b5950 | ||
|
|
09199c10df | ||
|
|
d05683262b | ||
|
|
13ffc8cf7f | ||
|
|
2e2496cd4c | ||
|
|
9c1766b806 | ||
|
|
68d9639a56 | ||
|
|
3c73bc9b55 | ||
|
|
38e72dc304 | ||
|
|
5ccb32f338 | ||
|
|
1506e22458 | ||
|
|
e7529e5487 | ||
|
|
a60e19fb54 | ||
|
|
43261b2347 | ||
|
|
579d0493d8 | ||
|
|
1a16e98d32 | ||
|
|
81c6d513a5 | ||
|
|
e75fbeb2e3 | ||
|
|
65c4160c8e | ||
|
|
13b482e075 | ||
|
|
1a3e1270dd | ||
|
|
2c381142d4 | ||
|
|
7c18bc5dfc | ||
|
|
be05a0699f | ||
|
|
0751dc3484 | ||
|
|
28cfadc593 | ||
|
|
018e566049 | ||
|
|
096d41c072 | ||
|
|
d97afb9d2a | ||
|
|
3c04878dfe | ||
|
|
9cb0c51573 | ||
|
|
9a13cd5fb7 | ||
|
|
01f1d0d06c | ||
|
|
f9e093efbb | ||
|
|
1fc0316541 | ||
|
|
4f7185b074 | ||
|
|
a84f2f40bb | ||
|
|
77a8e72c1f | ||
|
|
18d2a1ea8c | ||
|
|
61918597b7 | ||
|
|
fc2ddfe46b | ||
|
|
25922c5b03 | ||
|
|
75d1fa2adc | ||
|
|
f725889de7 | ||
|
|
133bd6f309 | ||
|
|
1669e90177 | ||
|
|
ee91f82171 | ||
|
|
187c6fbca1 | ||
|
|
2d0f636b5d | ||
|
|
b9f7bed8e9 | ||
|
|
f1d0a49fb3 | ||
|
|
f10f59204f | ||
|
|
0d3e9a56ee | ||
|
|
efc79c9ad3 | ||
|
|
4f39d1f922 | ||
|
|
0368202f2b | ||
|
|
01bfefdd8b | ||
|
|
4f2187c536 | ||
|
|
0a5b069cdd | ||
|
|
6f12132e88 | ||
|
|
ecae8b0288 |
26 changed files with 1851 additions and 481 deletions
9
.gitignore
vendored
9
.gitignore
vendored
|
|
@ -1,8 +1,9 @@
|
||||||
*~
|
*~
|
||||||
|
*.sw[op]
|
||||||
|
*.py[cod]
|
||||||
|
*.egg
|
||||||
*.egg-info
|
*.egg-info
|
||||||
*.pyc
|
|
||||||
*.swo
|
|
||||||
*.swp
|
|
||||||
.coverage
|
|
||||||
build
|
build
|
||||||
dist
|
dist
|
||||||
|
sdist
|
||||||
|
.coverage
|
||||||
|
|
|
||||||
19
.travis.yml
Normal file
19
.travis.yml
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
language: python
|
||||||
|
python:
|
||||||
|
- 2.6
|
||||||
|
- 2.7
|
||||||
|
- 3.4
|
||||||
|
before_install:
|
||||||
|
- sudo apt-get update
|
||||||
|
- sudo apt-get install nodejs
|
||||||
|
install:
|
||||||
|
- npm install -G socket.io
|
||||||
|
- npm install -G yargs
|
||||||
|
- pip install -U requests
|
||||||
|
- pip install -U six
|
||||||
|
- pip install -U websocket-client
|
||||||
|
- pip install -U coverage
|
||||||
|
before_script:
|
||||||
|
- DEBUG=* node socketIO_client/tests/serve.js &
|
||||||
|
- sleep 3
|
||||||
|
script: nosetests
|
||||||
67
CHANGES.rst
67
CHANGES.rst
|
|
@ -1,8 +1,69 @@
|
||||||
|
0.6.5
|
||||||
|
-----
|
||||||
|
- Updated wait loop to be more responsive under websocket transport
|
||||||
|
|
||||||
|
0.6.4
|
||||||
|
-----
|
||||||
|
- Fixed support for Python 3
|
||||||
|
- Fixed thread cleanup
|
||||||
|
|
||||||
|
0.6.3
|
||||||
|
-----
|
||||||
|
- Upgraded to socket.io protocol 1.x for websocket transport
|
||||||
|
- Added locks to fix concurrency issues with polling transport
|
||||||
|
- Fixed SSL support
|
||||||
|
|
||||||
|
0.6.1
|
||||||
|
-----
|
||||||
|
- Upgraded to socket.io protocol 1.x thanks to Sean Arietta and Joe Palmer
|
||||||
|
|
||||||
|
0.5.6
|
||||||
|
-----
|
||||||
|
- Backported to support requests 0.8.2
|
||||||
|
|
||||||
|
0.5.5
|
||||||
|
-----
|
||||||
|
- Fixed reconnection in the event of server restart
|
||||||
|
- Fixed calling on_reconnect() so that it is actually called
|
||||||
|
- Set default Namespace=None
|
||||||
|
- Added support for Python 3.4
|
||||||
|
|
||||||
|
0.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
|
0.3
|
||||||
---
|
---
|
||||||
- Added support for secure connections
|
- Added support for secure connections
|
||||||
- Added socketIO.wait()
|
- Added socketIO.wait()
|
||||||
- Improved exception handling in heartbeatThread and namespaceThread
|
- Improved exception handling in _RhythmicThread and _ListenerThread
|
||||||
|
|
||||||
0.2
|
0.2
|
||||||
---
|
---
|
||||||
|
|
@ -11,7 +72,5 @@
|
||||||
|
|
||||||
0.1
|
0.1
|
||||||
---
|
---
|
||||||
- Wrapped code from StackOverflow_
|
- Wrapped `code from StackOverflow <http://stackoverflow.com/questions/6692908/formatting-messages-to-send-to-socket-io-node-js-server-from-python-client>`_
|
||||||
- Added exception handling to destructor in case of connection failure
|
- 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
|
|
||||||
|
|
|
||||||
20
LICENSE
20
LICENSE
|
|
@ -1,7 +1,19 @@
|
||||||
Copyright (c) 2012 Roy Hyunjin Han and contributors
|
Copyright (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:
|
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 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.
|
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.
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
recursive-include socketIOClient *
|
recursive-include socketIO_client *
|
||||||
include *.rst
|
include *.html *.js *.rst
|
||||||
global-exclude *.pyc
|
global-exclude *.pyc
|
||||||
|
|
|
||||||
160
README.rst
160
README.rst
|
|
@ -1,17 +1,17 @@
|
||||||
|
.. image:: https://travis-ci.org/invisibleroads/socketIO-client.svg?branch=master
|
||||||
|
:target: https://travis-ci.org/invisibleroads/socketIO-client
|
||||||
|
|
||||||
|
|
||||||
socketIO-client
|
socketIO-client
|
||||||
===============
|
===============
|
||||||
Here is a socket.io_ client library for Python. You can use it to write test code for your socket.io_ server.
|
Here is a `socket.io <http://socket.io>`_ client library for Python. You can use it to write test code for your socket.io server.
|
||||||
|
|
||||||
Thanks to rod_ for the `StackOverflow question and answer`__ on which this code is based.
|
Please note that this version implements `socket.io protocol 1.x <https://github.com/automattic/socket.io-protocol>`_, which is not backwards compatible. If you want to communicate using `socket.io protocol 0.9 <https://github.com/learnboost/socket.io-spec>`_ (which is compatible with `gevent-socketio <https://github.com/abourget/gevent-socketio>`_), please use `socketIO-client 0.5.6 <https://pypi.python.org/pypi/socketIO-client/0.5.6>`_.
|
||||||
|
|
||||||
Thanks to liris_ for websocket-client_ and to guille_ for the `socket.io specification`_.
|
|
||||||
|
|
||||||
Thanks to `Paul Kienzle`_, `Josh VanderLinden`_, `Ian Fitzpatrick`_ for submitting code to expand support of the socket.io protocol.
|
|
||||||
|
|
||||||
|
|
||||||
Installation
|
Installation
|
||||||
------------
|
------------
|
||||||
::
|
Install the package in an isolated environment. ::
|
||||||
|
|
||||||
VIRTUAL_ENV=$HOME/.virtualenv
|
VIRTUAL_ENV=$HOME/.virtualenv
|
||||||
|
|
||||||
|
|
@ -22,7 +22,7 @@ Installation
|
||||||
source $VIRTUAL_ENV/bin/activate
|
source $VIRTUAL_ENV/bin/activate
|
||||||
|
|
||||||
# Install package
|
# Install package
|
||||||
easy_install -U socketIO-client
|
pip install -U socketIO-client
|
||||||
|
|
||||||
|
|
||||||
Usage
|
Usage
|
||||||
|
|
@ -32,35 +32,52 @@ Activate isolated environment. ::
|
||||||
VIRTUAL_ENV=$HOME/.virtualenv
|
VIRTUAL_ENV=$HOME/.virtualenv
|
||||||
source $VIRTUAL_ENV/bin/activate
|
source $VIRTUAL_ENV/bin/activate
|
||||||
|
|
||||||
|
Launch your socket.io server. ::
|
||||||
|
|
||||||
|
# Get package folder
|
||||||
|
PACKAGE_FOLDER=`python -c "import os, socketIO_client;\
|
||||||
|
print(os.path.dirname(socketIO_client.__file__))"`
|
||||||
|
# Start socket.io server
|
||||||
|
DEBUG=* node $PACKAGE_FOLDER/tests/serve.js
|
||||||
|
# Start proxy server in a separate terminal on the same machine
|
||||||
|
DEBUG=* node $PACKAGE_FOLDER/tests/proxy.js
|
||||||
|
|
||||||
|
For debugging information, run these commands first. ::
|
||||||
|
|
||||||
|
import logging
|
||||||
|
logging.getLogger('requests').setLevel(logging.WARNING)
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
Emit. ::
|
Emit. ::
|
||||||
|
|
||||||
from socketIO_client import SocketIO
|
from socketIO_client import SocketIO, LoggingNamespace
|
||||||
|
|
||||||
socketIO = SocketIO('localhost', 8000)
|
with SocketIO('localhost', 8000, LoggingNamespace) as socketIO:
|
||||||
socketIO.emit('aaa', {'bbb': 'ccc'})
|
socketIO.emit('aaa')
|
||||||
socketIO.wait(seconds=1)
|
socketIO.wait(seconds=1)
|
||||||
|
|
||||||
Emit with callback. ::
|
Emit with callback. ::
|
||||||
|
|
||||||
from socketIO_client import SocketIO
|
from socketIO_client import SocketIO, LoggingNamespace
|
||||||
|
|
||||||
def on_response(*args):
|
def on_bbb_response(*args):
|
||||||
print args
|
print('on_bbb_response', args)
|
||||||
|
|
||||||
socketIO = SocketIO('localhost', 8000)
|
with SocketIO('localhost', 8000, LoggingNamespace) as socketIO:
|
||||||
socketIO.emit('aaa', {'bbb': 'ccc'}, on_response)
|
socketIO.emit('bbb', {'xxx': 'yyy'}, on_bbb_response)
|
||||||
socketIO.wait(forCallbacks=True)
|
socketIO.wait_for_callbacks(seconds=1)
|
||||||
|
|
||||||
Define events. ::
|
Define events. ::
|
||||||
|
|
||||||
from socketIO_client import SocketIO
|
from socketIO_client import SocketIO, LoggingNamespace
|
||||||
|
|
||||||
def on_ddd(*args):
|
def on_aaa_response(*args):
|
||||||
print args
|
print('on_aaa_response', args)
|
||||||
|
|
||||||
socketIO = SocketIO('localhost', 8000)
|
socketIO = SocketIO('localhost', 8000, LoggingNamespace)
|
||||||
socketIO.on('ddd', on_ddd)
|
socketIO.on('aaa_response', on_aaa_response)
|
||||||
socketIO.wait()
|
socketIO.emit('aaa')
|
||||||
|
socketIO.wait(seconds=1)
|
||||||
|
|
||||||
Define events in a namespace. ::
|
Define events in a namespace. ::
|
||||||
|
|
||||||
|
|
@ -68,11 +85,13 @@ Define events in a namespace. ::
|
||||||
|
|
||||||
class Namespace(BaseNamespace):
|
class Namespace(BaseNamespace):
|
||||||
|
|
||||||
def on_ddd(self, *args):
|
def on_aaa_response(self, *args):
|
||||||
self.socketIO.emit('eee', {'fff': 'ggg'})
|
print('on_aaa_response', args)
|
||||||
|
self.emit('bbb')
|
||||||
|
|
||||||
socketIO = SocketIO('localhost', 8000, Namespace)
|
socketIO = SocketIO('localhost', 8000, Namespace)
|
||||||
socketIO.wait()
|
socketIO.emit('aaa')
|
||||||
|
socketIO.wait(seconds=1)
|
||||||
|
|
||||||
Define standard events. ::
|
Define standard events. ::
|
||||||
|
|
||||||
|
|
@ -80,44 +99,58 @@ Define standard events. ::
|
||||||
|
|
||||||
class Namespace(BaseNamespace):
|
class Namespace(BaseNamespace):
|
||||||
|
|
||||||
def on_connect(self, socketIO):
|
def on_connect(self):
|
||||||
print '[Connected]'
|
print('[Connected]')
|
||||||
|
|
||||||
def on_disconnect(self):
|
|
||||||
print '[Disconnected]'
|
|
||||||
|
|
||||||
def on_error(self, name, message):
|
|
||||||
print '[Error] %s: %s' % (name, message)
|
|
||||||
|
|
||||||
def on_message(self, id, message):
|
|
||||||
print '[Message] %s: %s' % (id, message)
|
|
||||||
|
|
||||||
socketIO = SocketIO('localhost', 8000, Namespace)
|
socketIO = SocketIO('localhost', 8000, Namespace)
|
||||||
socketIO.wait()
|
socketIO.wait(seconds=1)
|
||||||
|
|
||||||
Define different behavior for different channels on a single socket. ::
|
Define different namespaces on a single socket. ::
|
||||||
|
|
||||||
from socketIO_client import SocketIO, BaseNamespace
|
from socketIO_client import SocketIO, BaseNamespace
|
||||||
|
|
||||||
class MainNamespace(BaseNamespace):
|
|
||||||
|
|
||||||
def on_aaa(self, *args):
|
|
||||||
print 'aaa', args
|
|
||||||
|
|
||||||
class ChatNamespace(BaseNamespace):
|
class ChatNamespace(BaseNamespace):
|
||||||
|
|
||||||
def on_bbb(self, *args):
|
def on_aaa_response(self, *args):
|
||||||
print 'bbb', args
|
print('on_aaa_response', args)
|
||||||
|
|
||||||
class NewsNamespace(BaseNamespace):
|
class NewsNamespace(BaseNamespace):
|
||||||
|
|
||||||
def on_ccc(self, *args):
|
def on_aaa_response(self, *args):
|
||||||
print 'ccc', args
|
print('on_aaa_response', args)
|
||||||
|
|
||||||
mainSocket = SocketIO('localhost', 8000, MainNamespace)
|
socketIO = SocketIO('localhost', 8000)
|
||||||
chatSocket = mainSocket.connect('/chat', ChatNamespace)
|
chat_namespace = socketIO.define(ChatNamespace, '/chat')
|
||||||
newsSocket = mainSocket.connect('/news', NewsNamespace)
|
news_namespace = socketIO.define(NewsNamespace, '/news')
|
||||||
mainSocket.wait()
|
|
||||||
|
chat_namespace.emit('aaa')
|
||||||
|
news_namespace.emit('aaa')
|
||||||
|
socketIO.wait(seconds=1)
|
||||||
|
|
||||||
|
Connect via SSL. ::
|
||||||
|
|
||||||
|
from socketIO_client import SocketIO
|
||||||
|
|
||||||
|
SocketIO('https://localhost', verify=False)
|
||||||
|
|
||||||
|
Specify params, headers, cookies, proxies thanks to the `requests <http://python-requests.org>`_ 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', 8000)
|
||||||
|
socketIO.wait()
|
||||||
|
|
||||||
|
|
||||||
License
|
License
|
||||||
|
|
@ -125,14 +158,13 @@ License
|
||||||
This software is available under the MIT License.
|
This software is available under the MIT License.
|
||||||
|
|
||||||
|
|
||||||
.. _socket.io: http://socket.io
|
Credits
|
||||||
.. _rod: http://stackoverflow.com/users/370115/rod
|
-------
|
||||||
.. _StackOverflowQA: http://stackoverflow.com/questions/6692908/formatting-messages-to-send-to-socket-io-node-js-server-from-python-client
|
- `Guillermo Rauch <https://github.com/rauchg>`_ wrote the `socket.io specification <https://github.com/automattic/socket.io-protocol>`_.
|
||||||
__ StackOverflowQA_
|
- `Hiroki Ohtani <https://github.com/liris>`_ wrote `websocket-client <https://github.com/liris/websocket-client>`_.
|
||||||
.. _liris: https://github.com/liris
|
- `rod <http://stackoverflow.com/users/370115/rod>`_ wrote a `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>`_.
|
||||||
.. _websocket-client: https://github.com/liris/websocket-client
|
- `Alexandre Bourget <https://github.com/abourget>`_ wrote `gevent-socketio <https://github.com/abourget/gevent-socketio>`_, which is a socket.io server written in Python.
|
||||||
.. _guille: https://github.com/guille
|
- `Paul Kienzle <https://github.com/pkienzle>`_, `Zac Lee <https://github.com/zratic>`_, `Josh VanderLinden <https://github.com/codekoala>`_, `Ian Fitzpatrick <https://github.com/ifitzpatrick>`_, `Lucas Klein <https://github.com/lukasklein>`_, `Rui Chicoria <https://github.com/rchicoria>`_, `Travis Odom <https://github.com/burstaholic>`_, `Patrick Huber <https://github.com/stackmagic>`_, `Brad Campbell <https://github.com/bradjc>`_, `Daniel <https://github.com/dabidan>`_, `Sean Arietta <https://github.com/sarietta>`_ submitted code to expand support of the socket.io protocol.
|
||||||
.. _socket.io specification: https://github.com/LearnBoost/socket.io-spec
|
- `Bernard Pratz <https://github.com/guyzmo>`_, `Francis Bull <https://github.com/franbull>`_ wrote prototypes to support xhr-polling and jsonp-polling.
|
||||||
.. _Paul Kienzle: https://github.com/pkienzle
|
- `Eric Chen <https://github.com/taiyangc>`_, `Denis Zinevich <https://github.com/dzinevich>`_, `Thiago Hersan <https://github.com/thiagohersan>`_, `Nayef Copty <https://github.com/nayefc>`_, `Jörgen Karlsson <https://github.com/jorgen-k>`_, `Branden Ghena <https://github.com/brghena>`_, `Tim Landscheidt <https://github.com/scfc>`_, `Matt Porritt <https://github.com/mattporritt>`_ suggested ways to make the connection more robust.
|
||||||
.. _Josh VanderLinden: https://github.com/codekoala
|
- `Merlijn van Deen <https://github.com/valhallasw>`_, `Frederic Sureau <https://github.com/fredericsureau>`_, `Marcus Cobden <https://github.com/leth>`_, `Drew Hutchison <https://github.com/drewhutchison>`_, `wuurrd <https://github.com/wuurrd>`_, `Adam Kecer <https://github.com/amfg>`_, `Alex Monk <https://github.com/Krenair>`_, `Vishal P R <https://github.com/vishalwy>`_, `John Vandenberg <https://github.com/jayvdb>`_, `Thomas Grainger <https://github.com/graingert>`_ proposed changes that make the library more friendly and practical for you!
|
||||||
.. _Ian Fitzpatrick: https://github.com/GraphEffect
|
|
||||||
|
|
|
||||||
48
TODO.goals
Normal file
48
TODO.goals
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
= Consider supporting both protocols in the same library
|
||||||
|
https://github.com/invisibleroads/socketIO-client/issues/95
|
||||||
|
Add binary support
|
||||||
|
https://github.com/invisibleroads/socketIO-client/pull/85
|
||||||
|
https://github.com/invisibleroads/socketIO-client/issues/70
|
||||||
|
https://github.com/invisibleroads/socketIO-client/issues/71
|
||||||
|
https://github.com/invisibleroads/socketIO-client/issues/91
|
||||||
|
Check requests dependency
|
||||||
|
https://github.com/invisibleroads/socketIO-client/issues/92
|
||||||
|
https://github.com/invisibleroads/socketIO-client/pull/93
|
||||||
|
https://github.com/invisibleroads/socketIO-client/commit/b288d89c15d452a30bfeb00f38494f59f71f5a43
|
||||||
|
Check SSL
|
||||||
|
https://github.com/invisibleroads/socketIO-client/issues/86
|
||||||
|
https://github.com/invisibleroads/socketIO-client/pull/87
|
||||||
|
Look into invalid namespace handling
|
||||||
|
https://github.com/invisibleroads/socketIO-client/issues/84
|
||||||
|
Check unicode issues
|
||||||
|
https://github.com/invisibleroads/socketIO-client/issues/81
|
||||||
|
Check python3 support for socketIO-client 0.5.6
|
||||||
|
https://github.com/invisibleroads/socketIO-client/issues/83
|
||||||
|
Check OK assertion
|
||||||
|
https://github.com/invisibleroads/socketIO-client/issues/99
|
||||||
|
https://github.com/invisibleroads/socketIO-client/pull/103
|
||||||
|
Check why connected=True after termination
|
||||||
|
https://github.com/invisibleroads/socketIO-client/issues/80
|
||||||
|
https://github.com/invisibleroads/socketIO-client/issues/98
|
||||||
|
Consider catching heartbeat thread exception
|
||||||
|
https://github.com/invisibleroads/socketIO-client/issues/100
|
||||||
|
Check why it blocks when defining a namespace
|
||||||
|
https://github.com/invisibleroads/socketIO-client/issues/96
|
||||||
|
Check why transports are not being set
|
||||||
|
https://github.com/invisibleroads/socketIO-client/issues/102
|
||||||
|
Look at 404 not found error
|
||||||
|
https://github.com/invisibleroads/socketIO-client/issues/101
|
||||||
|
Look at socketio off and socketio once
|
||||||
|
https://github.com/invisibleroads/socketIO-client/pull/94
|
||||||
|
Implement rooms
|
||||||
|
https://github.com/invisibleroads/socketIO-client/issues/72
|
||||||
|
https://github.com/invisibleroads/socketIO-client/pull/65
|
||||||
|
Check tests
|
||||||
|
https://github.com/invisibleroads/socketIO-client/issues/90
|
||||||
|
Check whether it works on Windows 8
|
||||||
|
https://github.com/invisibleroads/socketIO-client/issues/97
|
||||||
|
Add debian packaging support
|
||||||
|
https://github.com/invisibleroads/socketIO-client/pull/89
|
||||||
|
|
||||||
|
+ Review issues and pull requests
|
||||||
|
+ Order issues
|
||||||
0
TODO.rst
0
TODO.rst
|
|
@ -1,29 +0,0 @@
|
||||||
'Launch this server in another terminal window before running tests'
|
|
||||||
from socketio import socketio_manage
|
|
||||||
from socketio.namespace import BaseNamespace
|
|
||||||
from socketio.server import SocketIOServer
|
|
||||||
|
|
||||||
|
|
||||||
class Namespace(BaseNamespace):
|
|
||||||
|
|
||||||
def on_aaa(self, *args):
|
|
||||||
self.socket.send_packet(dict(
|
|
||||||
type='event',
|
|
||||||
name='ddd',
|
|
||||||
args=args,
|
|
||||||
endpoint=self.ns_name))
|
|
||||||
|
|
||||||
|
|
||||||
class Application(object):
|
|
||||||
|
|
||||||
def __call__(self, environ, start_response):
|
|
||||||
socketio_manage(environ, {
|
|
||||||
'': Namespace,
|
|
||||||
'/chat': Namespace,
|
|
||||||
'/news': Namespace,
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
socketIOServer = SocketIOServer(('0.0.0.0', 8000), Application())
|
|
||||||
socketIOServer.serve_forever()
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
[nosetests]
|
[nosetests]
|
||||||
detailed-errors=TRUE
|
detailed-errors = TRUE
|
||||||
with-coverage=TRUE
|
with-coverage = TRUE
|
||||||
cover-package=socketIO_client
|
cover-package = socketIO_client
|
||||||
cover-erase=TRUE
|
cover-erase = TRUE
|
||||||
|
|
|
||||||
35
setup.py
Executable file → Normal file
35
setup.py
Executable file → Normal file
|
|
@ -1,31 +1,42 @@
|
||||||
import os
|
import io
|
||||||
from setuptools import setup, find_packages
|
from os.path import abspath, dirname, join
|
||||||
|
from setuptools import find_packages, setup
|
||||||
|
|
||||||
|
|
||||||
here = os.path.abspath(os.path.dirname(__file__))
|
REQUIREMENTS = [
|
||||||
README = open(os.path.join(here, 'README.rst')).read()
|
'requests',
|
||||||
CHANGES = open(os.path.join(here, 'CHANGES.rst')).read()
|
'six',
|
||||||
|
'websocket-client',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
HERE = dirname(abspath(__file__))
|
||||||
|
LOAD_TEXT = lambda name: io.open(join(HERE, name), encoding='UTF-8').read()
|
||||||
|
DESCRIPTION = '\n\n'.join(LOAD_TEXT(_) for _ in [
|
||||||
|
'README.rst',
|
||||||
|
'CHANGES.rst',
|
||||||
|
])
|
||||||
setup(
|
setup(
|
||||||
name='socketIO-client',
|
name='socketIO_client',
|
||||||
version='0.3',
|
version='0.6.5',
|
||||||
description='A socket.io client library',
|
description='A socket.io client library',
|
||||||
long_description=README + '\n\n' + CHANGES,
|
long_description=DESCRIPTION,
|
||||||
license='MIT',
|
license='MIT',
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Intended Audience :: Developers',
|
'Intended Audience :: Developers',
|
||||||
'Programming Language :: Python',
|
'Programming Language :: Python',
|
||||||
'License :: OSI Approved :: MIT License',
|
'License :: OSI Approved :: MIT License',
|
||||||
|
'Development Status :: 5 - Production/Stable',
|
||||||
],
|
],
|
||||||
keywords='socket.io node.js',
|
keywords='socket.io node.js',
|
||||||
author='Roy Hyunjin Han',
|
author='Roy Hyunjin Han',
|
||||||
author_email='rhh@crosscompute.com',
|
author_email='rhh@crosscompute.com',
|
||||||
url='https://github.com/invisibleroads/socketIO-client',
|
url='https://github.com/invisibleroads/socketIO-client',
|
||||||
install_requires=[
|
install_requires=REQUIREMENTS,
|
||||||
'anyjson',
|
tests_require=[
|
||||||
'websocket-client',
|
'nose',
|
||||||
|
'coverage',
|
||||||
],
|
],
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
zip_safe=True)
|
zip_safe=False)
|
||||||
|
|
|
||||||
|
|
@ -1,335 +1,515 @@
|
||||||
import websocket
|
from .exceptions import ConnectionError, TimeoutError, PacketError
|
||||||
from anyjson import dumps, loads
|
from .heartbeats import HeartbeatThread
|
||||||
from threading import Thread, Event
|
from .logs import LoggingMixin
|
||||||
from time import sleep
|
from .namespaces import (
|
||||||
from urllib import urlopen
|
EngineIONamespace, SocketIONamespace, LoggingSocketIONamespace,
|
||||||
|
find_callback)
|
||||||
|
from .parsers import (
|
||||||
|
parse_host, parse_engineIO_session,
|
||||||
|
format_socketIO_packet_data, parse_socketIO_packet_data,
|
||||||
|
get_namespace_path)
|
||||||
|
from .symmetries import get_character
|
||||||
|
from .transports import (
|
||||||
|
WebsocketTransport, XHR_PollingTransport, prepare_http_session, TRANSPORTS)
|
||||||
|
|
||||||
|
|
||||||
__version__ = '0.3'
|
__all__ = 'SocketIO', 'SocketIONamespace'
|
||||||
|
__version__ = '0.6.3'
|
||||||
|
BaseNamespace = SocketIONamespace
|
||||||
|
LoggingNamespace = LoggingSocketIONamespace
|
||||||
|
|
||||||
|
|
||||||
PROTOCOL = 1 # SocketIO protocol version
|
def retry(f):
|
||||||
|
def wrap(*args, **kw):
|
||||||
|
self = args[0]
|
||||||
class BaseNamespace(object): # pragma: no cover
|
|
||||||
|
|
||||||
def __init__(self, socketIO):
|
|
||||||
self.socketIO = socketIO
|
|
||||||
|
|
||||||
def on_connect(self, socketIO):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def on_disconnect(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def on_error(self, reason, advice):
|
|
||||||
print '[Error] %s' % advice
|
|
||||||
|
|
||||||
def on_message(self, messageData):
|
|
||||||
print '[Message] %s' % messageData
|
|
||||||
|
|
||||||
def on_(self, eventName, *eventArguments):
|
|
||||||
print '[Event] %s%s' % (eventName, eventArguments)
|
|
||||||
|
|
||||||
def on_open(self, *args):
|
|
||||||
print '[Open]', args
|
|
||||||
|
|
||||||
def on_close(self, *args):
|
|
||||||
print '[Close]', args
|
|
||||||
|
|
||||||
def on_retry(self, *args):
|
|
||||||
print '[Retry]', args
|
|
||||||
|
|
||||||
def on_reconnect(self, *args):
|
|
||||||
print '[Reconnect]', args
|
|
||||||
|
|
||||||
|
|
||||||
class SocketIO(object):
|
|
||||||
|
|
||||||
messageID = 0
|
|
||||||
|
|
||||||
def __init__(self, host, port, Namespace=BaseNamespace, secure=False):
|
|
||||||
self.host = host
|
|
||||||
self.port = int(port)
|
|
||||||
self.namespace = Namespace(self)
|
|
||||||
self.secure = secure
|
|
||||||
self.__connect()
|
|
||||||
|
|
||||||
heartbeatInterval = self.heartbeatTimeout - 2
|
|
||||||
self.heartbeatThread = RhythmicThread(heartbeatInterval,
|
|
||||||
self._send_heartbeat)
|
|
||||||
self.heartbeatThread.start()
|
|
||||||
|
|
||||||
self.channelByName = {}
|
|
||||||
self.callbackByEvent = {}
|
|
||||||
self.namespaceThread = ListenerThread(self)
|
|
||||||
self.namespaceThread.start()
|
|
||||||
|
|
||||||
def __del__(self): # pragma: no cover
|
|
||||||
self.heartbeatThread.cancel()
|
|
||||||
self.namespaceThread.cancel()
|
|
||||||
self.connection.close()
|
|
||||||
|
|
||||||
def __connect(self):
|
|
||||||
baseURL = '%s:%d/socket.io/%s' % (self.host, self.port, PROTOCOL)
|
|
||||||
try:
|
try:
|
||||||
response = urlopen('%s://%s/' % (
|
return f(*args, **kw)
|
||||||
'https' if self.secure else 'http', baseURL))
|
except (TimeoutError, ConnectionError):
|
||||||
except IOError: # pragma: no cover
|
self._opened = False
|
||||||
raise SocketIOError('Could not start connection')
|
return f(*args, **kw)
|
||||||
if 200 != response.getcode(): # pragma: no cover
|
return wrap
|
||||||
raise SocketIOError('Could not establish connection')
|
|
||||||
responseParts = response.readline().split(':')
|
|
||||||
self.sessionID = responseParts[0]
|
|
||||||
self.heartbeatTimeout = int(responseParts[1])
|
|
||||||
self.connectionTimeout = int(responseParts[2])
|
|
||||||
self.supportedTransports = responseParts[3].split(',')
|
|
||||||
if 'websocket' not in self.supportedTransports:
|
|
||||||
raise SocketIOError('Could not parse handshake') # pragma: no cover
|
|
||||||
socketURL = '%s://%s/websocket/%s' % (
|
|
||||||
'wss' if self.secure else 'ws', baseURL, self.sessionID)
|
|
||||||
self.connection = websocket.create_connection(socketURL)
|
|
||||||
|
|
||||||
def _recv_packet(self):
|
|
||||||
code, packetID, channelName, data = -1, None, None, None
|
|
||||||
packet = self.connection.recv()
|
|
||||||
packetParts = packet.split(':', 3)
|
|
||||||
packetCount = len(packetParts)
|
|
||||||
if 4 == packetCount:
|
|
||||||
code, packetID, channelName, data = packetParts
|
|
||||||
elif 3 == packetCount:
|
|
||||||
code, packetID, channelName = packetParts
|
|
||||||
elif 1 == packetCount: # pragma: no cover
|
|
||||||
code = packetParts[0]
|
|
||||||
return int(code), packetID, channelName, data
|
|
||||||
|
|
||||||
def _send_packet(self, code, channelName='', data='', callback=None):
|
class EngineIO(LoggingMixin):
|
||||||
self.connection.send(':'.join([
|
|
||||||
str(code),
|
|
||||||
self.set_callback(callback) if callback else '',
|
|
||||||
channelName,
|
|
||||||
data]))
|
|
||||||
|
|
||||||
def disconnect(self, channelName=''):
|
def __init__(
|
||||||
self._send_packet(0, channelName)
|
self, host, port=None, Namespace=EngineIONamespace,
|
||||||
if channelName:
|
wait_for_connection=True, transports=TRANSPORTS,
|
||||||
del self.channelByName[channelName]
|
resource='engine.io', hurry_interval_in_seconds=1, **kw):
|
||||||
|
self._is_secure, self._url = parse_host(host, port, resource)
|
||||||
|
self._wait_for_connection = wait_for_connection
|
||||||
|
self._client_transports = transports
|
||||||
|
self._hurry_interval_in_seconds = hurry_interval_in_seconds
|
||||||
|
self._http_session = prepare_http_session(kw)
|
||||||
|
|
||||||
|
self._log_name = self._url
|
||||||
|
self._wants_to_close = False
|
||||||
|
self._opened = False
|
||||||
|
|
||||||
|
if Namespace:
|
||||||
|
self.define(Namespace)
|
||||||
|
self._transport
|
||||||
|
|
||||||
|
# Connect
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _transport(self):
|
||||||
|
if self._opened:
|
||||||
|
return self._transport_instance
|
||||||
|
self._engineIO_session = self._get_engineIO_session()
|
||||||
|
self._negotiate_transport()
|
||||||
|
self._connect_namespaces()
|
||||||
|
self._opened = True
|
||||||
|
self._reset_heartbeat()
|
||||||
|
return self._transport_instance
|
||||||
|
|
||||||
|
def _get_engineIO_session(self):
|
||||||
|
warning_screen = self._yield_warning_screen()
|
||||||
|
for elapsed_time in warning_screen:
|
||||||
|
transport = XHR_PollingTransport(
|
||||||
|
self._http_session, self._is_secure, self._url)
|
||||||
|
try:
|
||||||
|
engineIO_packet_type, engineIO_packet_data = next(
|
||||||
|
transport.recv_packet())
|
||||||
|
break
|
||||||
|
except (TimeoutError, ConnectionError) as e:
|
||||||
|
if not self._wait_for_connection:
|
||||||
|
raise
|
||||||
|
warning = Exception('[waiting for connection] %s' % e)
|
||||||
|
warning_screen.throw(warning)
|
||||||
|
assert engineIO_packet_type == 0 # engineIO_packet_type == open
|
||||||
|
return parse_engineIO_session(engineIO_packet_data)
|
||||||
|
|
||||||
|
def _negotiate_transport(self):
|
||||||
|
self._transport_instance = self._get_transport('xhr-polling')
|
||||||
|
self.transport_name = 'xhr-polling'
|
||||||
|
is_ws_client = 'websocket' in self._client_transports
|
||||||
|
is_ws_server = 'websocket' in self._engineIO_session.transport_upgrades
|
||||||
|
if is_ws_client and is_ws_server:
|
||||||
|
try:
|
||||||
|
transport = self._get_transport('websocket')
|
||||||
|
transport.send_packet(2, 'probe')
|
||||||
|
for packet_type, packet_data in transport.recv_packet():
|
||||||
|
if packet_type == 3 and packet_data == b'probe':
|
||||||
|
transport.send_packet(5, '')
|
||||||
|
self._transport_instance = transport
|
||||||
|
self.transport_name = 'websocket'
|
||||||
|
else:
|
||||||
|
self._warn('unexpected engine.io packet')
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._debug('[transport selected] %s', self.transport_name)
|
||||||
|
|
||||||
|
def _reset_heartbeat(self):
|
||||||
|
try:
|
||||||
|
self._heartbeat_thread.halt()
|
||||||
|
hurried = self._heartbeat_thread.hurried
|
||||||
|
except AttributeError:
|
||||||
|
hurried = False
|
||||||
|
ping_interval = self._engineIO_session.ping_interval
|
||||||
|
if self.transport_name.endswith('-polling'):
|
||||||
|
# Use ping/pong to unblock recv for polling transport
|
||||||
|
hurry_interval_in_seconds = self._hurry_interval_in_seconds
|
||||||
else:
|
else:
|
||||||
self.__del__()
|
# Use timeout to unblock recv for websocket transport
|
||||||
|
hurry_interval_in_seconds = ping_interval
|
||||||
|
self._heartbeat_thread = HeartbeatThread(
|
||||||
|
send_heartbeat=self._ping,
|
||||||
|
relax_interval_in_seconds=ping_interval,
|
||||||
|
hurry_interval_in_seconds=hurry_interval_in_seconds)
|
||||||
|
self._heartbeat_thread.start()
|
||||||
|
if hurried:
|
||||||
|
self._heartbeat_thread.hurry()
|
||||||
|
self._debug('[heartbeat reset]')
|
||||||
|
|
||||||
|
def _connect_namespaces(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _get_transport(self, transport_name):
|
||||||
|
SelectedTransport = {
|
||||||
|
'xhr-polling': XHR_PollingTransport,
|
||||||
|
'websocket': WebsocketTransport,
|
||||||
|
}[transport_name]
|
||||||
|
return SelectedTransport(
|
||||||
|
self._http_session, self._is_secure, self._url,
|
||||||
|
self._engineIO_session)
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *exception_pack):
|
||||||
|
self._close()
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
self._close()
|
||||||
|
|
||||||
|
# Define
|
||||||
|
|
||||||
|
def define(self, Namespace):
|
||||||
|
self._namespace = namespace = Namespace(self)
|
||||||
|
return namespace
|
||||||
|
|
||||||
|
def on(self, event, callback):
|
||||||
|
try:
|
||||||
|
namespace = self.get_namespace()
|
||||||
|
except PacketError:
|
||||||
|
namespace = self.define(EngineIONamespace)
|
||||||
|
return namespace.on(event, callback)
|
||||||
|
|
||||||
|
def get_namespace(self):
|
||||||
|
try:
|
||||||
|
return self._namespace
|
||||||
|
except AttributeError:
|
||||||
|
raise PacketError('undefined engine.io namespace')
|
||||||
|
|
||||||
|
# Act
|
||||||
|
|
||||||
|
def send(self, engineIO_packet_data):
|
||||||
|
self._message(engineIO_packet_data)
|
||||||
|
|
||||||
|
def _open(self):
|
||||||
|
engineIO_packet_type = 0
|
||||||
|
self._transport_instance.send_packet(engineIO_packet_type)
|
||||||
|
|
||||||
|
def _close(self):
|
||||||
|
self._wants_to_close = True
|
||||||
|
try:
|
||||||
|
self._heartbeat_thread.halt()
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
if not self._opened:
|
||||||
|
return
|
||||||
|
engineIO_packet_type = 1
|
||||||
|
try:
|
||||||
|
self._transport_instance.send_packet(engineIO_packet_type)
|
||||||
|
except (TimeoutError, ConnectionError):
|
||||||
|
pass
|
||||||
|
self._opened = False
|
||||||
|
|
||||||
|
def _ping(self, engineIO_packet_data=''):
|
||||||
|
engineIO_packet_type = 2
|
||||||
|
self._transport_instance.send_packet(
|
||||||
|
engineIO_packet_type, engineIO_packet_data)
|
||||||
|
|
||||||
|
def _pong(self, engineIO_packet_data=''):
|
||||||
|
engineIO_packet_type = 3
|
||||||
|
self._transport_instance.send_packet(
|
||||||
|
engineIO_packet_type, engineIO_packet_data)
|
||||||
|
|
||||||
|
@retry
|
||||||
|
def _message(self, engineIO_packet_data, with_transport_instance=False):
|
||||||
|
engineIO_packet_type = 4
|
||||||
|
if with_transport_instance:
|
||||||
|
transport = self._transport_instance
|
||||||
|
else:
|
||||||
|
transport = self._transport
|
||||||
|
transport.send_packet(engineIO_packet_type, engineIO_packet_data)
|
||||||
|
self._debug('[socket.io packet sent] %s', engineIO_packet_data)
|
||||||
|
|
||||||
|
def _upgrade(self):
|
||||||
|
engineIO_packet_type = 5
|
||||||
|
self._transport_instance.send_packet(engineIO_packet_type)
|
||||||
|
|
||||||
|
def _noop(self):
|
||||||
|
engineIO_packet_type = 6
|
||||||
|
self._transport_instance.send_packet(engineIO_packet_type)
|
||||||
|
|
||||||
|
# React
|
||||||
|
|
||||||
|
def wait(self, seconds=None, **kw):
|
||||||
|
'Wait in a loop and react to events as defined in the namespaces'
|
||||||
|
# Use ping/pong to unblock recv for polling transport
|
||||||
|
self._heartbeat_thread.hurry()
|
||||||
|
# Use timeout to unblock recv for websocket transport
|
||||||
|
self._transport.set_timeout(seconds=1)
|
||||||
|
# Listen
|
||||||
|
warning_screen = self._yield_warning_screen(seconds)
|
||||||
|
for elapsed_time in warning_screen:
|
||||||
|
if self._should_stop_waiting(**kw):
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
self._process_packets()
|
||||||
|
except TimeoutError:
|
||||||
|
pass
|
||||||
|
except ConnectionError as e:
|
||||||
|
self._opened = False
|
||||||
|
try:
|
||||||
|
warning = Exception('[connection error] %s' % e)
|
||||||
|
warning_screen.throw(warning)
|
||||||
|
except StopIteration:
|
||||||
|
self._warn(warning)
|
||||||
|
try:
|
||||||
|
namespace = self.get_namespace()
|
||||||
|
namespace.on_disconnect()
|
||||||
|
except PacketError:
|
||||||
|
pass
|
||||||
|
self._heartbeat_thread.relax()
|
||||||
|
self._transport.set_timeout()
|
||||||
|
|
||||||
|
def _should_stop_waiting(self):
|
||||||
|
return self._wants_to_close
|
||||||
|
|
||||||
|
def _process_packets(self):
|
||||||
|
for engineIO_packet in self._transport.recv_packet():
|
||||||
|
try:
|
||||||
|
self._process_packet(engineIO_packet)
|
||||||
|
except PacketError as e:
|
||||||
|
self._warn('[packet error] %s', e)
|
||||||
|
|
||||||
|
def _process_packet(self, packet):
|
||||||
|
engineIO_packet_type, engineIO_packet_data = packet
|
||||||
|
# Launch callbacks
|
||||||
|
namespace = self.get_namespace()
|
||||||
|
try:
|
||||||
|
delegate = {
|
||||||
|
0: self._on_open,
|
||||||
|
1: self._on_close,
|
||||||
|
2: self._on_ping,
|
||||||
|
3: self._on_pong,
|
||||||
|
4: self._on_message,
|
||||||
|
5: self._on_upgrade,
|
||||||
|
6: self._on_noop,
|
||||||
|
}[engineIO_packet_type]
|
||||||
|
except KeyError:
|
||||||
|
raise PacketError(
|
||||||
|
'unexpected engine.io packet type (%s)' % engineIO_packet_type)
|
||||||
|
delegate(engineIO_packet_data, namespace)
|
||||||
|
if engineIO_packet_type is 4:
|
||||||
|
return engineIO_packet_data
|
||||||
|
|
||||||
|
def _on_open(self, data, namespace):
|
||||||
|
namespace._find_packet_callback('open')()
|
||||||
|
|
||||||
|
def _on_close(self, data, namespace):
|
||||||
|
namespace._find_packet_callback('close')()
|
||||||
|
|
||||||
|
def _on_ping(self, data, namespace):
|
||||||
|
self._pong(data)
|
||||||
|
namespace._find_packet_callback('ping')(data)
|
||||||
|
|
||||||
|
def _on_pong(self, data, namespace):
|
||||||
|
namespace._find_packet_callback('pong')(data)
|
||||||
|
|
||||||
|
def _on_message(self, data, namespace):
|
||||||
|
namespace._find_packet_callback('message')(data)
|
||||||
|
|
||||||
|
def _on_upgrade(self, data, namespace):
|
||||||
|
namespace._find_packet_callback('upgrade')()
|
||||||
|
|
||||||
|
def _on_noop(self, data, namespace):
|
||||||
|
namespace._find_packet_callback('noop')()
|
||||||
|
|
||||||
|
|
||||||
|
class SocketIO(EngineIO):
|
||||||
|
"""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 desired transports=['websocket', 'xhr-polling'].
|
||||||
|
- 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=SocketIONamespace,
|
||||||
|
wait_for_connection=True, transports=TRANSPORTS,
|
||||||
|
resource='socket.io', hurry_interval_in_seconds=1, **kw):
|
||||||
|
self._namespace_by_path = {}
|
||||||
|
self._callback_by_ack_id = {}
|
||||||
|
self._ack_id = 0
|
||||||
|
super(SocketIO, self).__init__(
|
||||||
|
host, port, Namespace, wait_for_connection, transports,
|
||||||
|
resource, hurry_interval_in_seconds, **kw)
|
||||||
|
|
||||||
|
# Connect
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def connected(self):
|
def connected(self):
|
||||||
return self.connection.connected
|
return self._opened
|
||||||
|
|
||||||
def connect(self, channelName, Namespace=BaseNamespace):
|
def _connect_namespaces(self):
|
||||||
channel = Channel(self, channelName, Namespace)
|
for path, namespace in self._namespace_by_path.items():
|
||||||
self.channelByName[channelName] = channel
|
namespace._transport = self._transport_instance
|
||||||
self._send_packet(1, channelName)
|
if path:
|
||||||
return channel
|
self.connect(path, with_transport_instance=True)
|
||||||
|
|
||||||
def _send_heartbeat(self):
|
def __exit__(self, *exception_pack):
|
||||||
|
self.disconnect()
|
||||||
|
super(SocketIO, self).__exit__(*exception_pack)
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
self.disconnect()
|
||||||
|
super(SocketIO, self).__del__()
|
||||||
|
|
||||||
|
# Define
|
||||||
|
|
||||||
|
def define(self, Namespace, path=''):
|
||||||
|
self._namespace_by_path[path] = namespace = Namespace(self, path)
|
||||||
|
if path:
|
||||||
|
self.connect(path)
|
||||||
|
self.wait(for_connect=True)
|
||||||
|
return namespace
|
||||||
|
|
||||||
|
def on(self, event, callback, path=''):
|
||||||
try:
|
try:
|
||||||
self._send_packet(2)
|
namespace = self.get_namespace(path)
|
||||||
except:
|
except PacketError:
|
||||||
self.__del__()
|
namespace = self.define(SocketIONamespace, path)
|
||||||
|
return namespace.on(event, callback)
|
||||||
|
|
||||||
def message(self, messageData, callback=None, channelName=''):
|
def get_namespace(self, path=''):
|
||||||
if isinstance(messageData, basestring):
|
|
||||||
code = 3
|
|
||||||
data = messageData
|
|
||||||
else:
|
|
||||||
code = 4
|
|
||||||
data = dumps(messageData)
|
|
||||||
self._send_packet(code, channelName, data, callback)
|
|
||||||
|
|
||||||
def emit(self, eventName, *eventArguments, **eventKeywords):
|
|
||||||
code = 5
|
|
||||||
if callable(eventArguments[-1]):
|
|
||||||
callback = eventArguments[-1]
|
|
||||||
eventArguments = eventArguments[:-1]
|
|
||||||
else:
|
|
||||||
callback = None
|
|
||||||
channelName = eventKeywords.get('channelName', '')
|
|
||||||
data = dumps(dict(name=eventName, args=eventArguments))
|
|
||||||
self._send_packet(code, channelName, data, callback)
|
|
||||||
|
|
||||||
def get_callback(self, channelName, eventName):
|
|
||||||
'Get callback associated with channelName and eventName'
|
|
||||||
socketIO = self.channelByName[channelName] if channelName else self
|
|
||||||
try:
|
try:
|
||||||
return socketIO.callbackByEvent[eventName]
|
return self._namespace_by_path[path]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
raise PacketError('undefined socket.io namespace (%s)' % path)
|
||||||
namespace = socketIO.namespace
|
|
||||||
|
|
||||||
def callback_(*eventArguments):
|
# Act
|
||||||
return namespace.on_(eventName, *eventArguments)
|
|
||||||
return getattr(namespace, name_callback(eventName), callback_)
|
|
||||||
|
|
||||||
def set_callback(self, callback):
|
def connect(self, path, with_transport_instance=False):
|
||||||
'Set callback that will be called after receiving an acknowledgment'
|
socketIO_packet_type = 0
|
||||||
self.messageID += 1
|
socketIO_packet_data = format_socketIO_packet_data(path)
|
||||||
self.namespaceThread.set_callback(self.messageID, callback)
|
self._message(
|
||||||
return '%s+' % self.messageID
|
str(socketIO_packet_type) + socketIO_packet_data,
|
||||||
|
with_transport_instance)
|
||||||
|
|
||||||
def on(self, eventName, callback):
|
def disconnect(self, path=''):
|
||||||
self.callbackByEvent[eventName] = callback
|
if not path or not self._opened:
|
||||||
|
self._close()
|
||||||
def wait(self, seconds=None, forCallbacks=False):
|
elif path:
|
||||||
if forCallbacks:
|
socketIO_packet_type = 1
|
||||||
self.namespaceThread.wait_for_callbacks(seconds)
|
socketIO_packet_data = format_socketIO_packet_data(path)
|
||||||
elif seconds:
|
|
||||||
sleep(seconds)
|
|
||||||
else:
|
|
||||||
try:
|
try:
|
||||||
while self.connected:
|
self._message(str(socketIO_packet_type) + socketIO_packet_data)
|
||||||
sleep(1)
|
except (TimeoutError, ConnectionError):
|
||||||
except KeyboardInterrupt:
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Channel(object):
|
|
||||||
|
|
||||||
def __init__(self, socketIO, channelName, Namespace):
|
|
||||||
self.socketIO = socketIO
|
|
||||||
self.channelName = channelName
|
|
||||||
self.namespace = Namespace(self)
|
|
||||||
self.callbackByEvent = {}
|
|
||||||
|
|
||||||
def disconnect(self):
|
|
||||||
self.socketIO.disconnect(self.channelName)
|
|
||||||
|
|
||||||
def emit(self, eventName, *eventArguments):
|
|
||||||
self.socketIO.emit(eventName, *eventArguments,
|
|
||||||
channelName=self.channelName)
|
|
||||||
|
|
||||||
def message(self, messageData, callback=None):
|
|
||||||
self.socketIO.message(messageData, callback,
|
|
||||||
channelName=self.channelName)
|
|
||||||
|
|
||||||
def on(self, eventName, eventCallback):
|
|
||||||
self.callbackByEvent[eventName] = eventCallback
|
|
||||||
|
|
||||||
|
|
||||||
class ListenerThread(Thread):
|
|
||||||
'Process messages from SocketIO server'
|
|
||||||
|
|
||||||
daemon = True
|
|
||||||
|
|
||||||
def __init__(self, socketIO):
|
|
||||||
super(ListenerThread, self).__init__()
|
|
||||||
self.socketIO = socketIO
|
|
||||||
self.done = Event()
|
|
||||||
self.waitingForCallbacks = Event()
|
|
||||||
self.callbackByMessageID = {}
|
|
||||||
self.get_callback = self.socketIO.get_callback
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
while not self.done.is_set():
|
|
||||||
try:
|
|
||||||
code, packetID, channelName, data = self.socketIO._recv_packet()
|
|
||||||
except:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
delegate = {
|
|
||||||
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_acknowledgment,
|
|
||||||
7: self.on_error,
|
|
||||||
}[code]
|
|
||||||
except KeyError:
|
|
||||||
continue
|
|
||||||
delegate(packetID, channelName, data)
|
|
||||||
|
|
||||||
def cancel(self):
|
|
||||||
self.done.set()
|
|
||||||
|
|
||||||
def wait_for_callbacks(self, seconds):
|
|
||||||
self.waitingForCallbacks.set()
|
|
||||||
self.join(seconds)
|
|
||||||
|
|
||||||
def set_callback(self, messageID, callback):
|
|
||||||
self.callbackByMessageID[messageID] = callback
|
|
||||||
|
|
||||||
def on_disconnect(self, packetID, channelName, data):
|
|
||||||
callback = self.get_callback(channelName, 'disconnect')
|
|
||||||
callback()
|
|
||||||
|
|
||||||
def on_connect(self, packetID, channelName, data):
|
|
||||||
callback = self.get_callback(channelName, 'connect')
|
|
||||||
callback(self.socketIO)
|
|
||||||
|
|
||||||
def on_heartbeat(self, packetID, channelName, data):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def on_message(self, packetID, channelName, data):
|
|
||||||
callback = self.get_callback(channelName, 'message')
|
|
||||||
callback(data)
|
|
||||||
|
|
||||||
def on_json(self, packetID, channelName, data):
|
|
||||||
callback = self.get_callback(channelName, 'message')
|
|
||||||
callback(loads(data))
|
|
||||||
|
|
||||||
def on_event(self, packetID, channelName, data):
|
|
||||||
valueByName = loads(data)
|
|
||||||
eventName = valueByName['name']
|
|
||||||
eventArguments = valueByName['args']
|
|
||||||
callback = self.get_callback(channelName, eventName)
|
|
||||||
callback(*eventArguments)
|
|
||||||
|
|
||||||
def on_acknowledgment(self, packetID, channelName, data):
|
|
||||||
dataParts = data.split('+', 1)
|
|
||||||
messageID = int(dataParts[0])
|
|
||||||
arguments = loads(dataParts[1]) or []
|
|
||||||
try:
|
try:
|
||||||
callback = self.callbackByMessageID[messageID]
|
namespace = self._namespace_by_path.pop(path)
|
||||||
|
namespace.on_disconnect()
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
else:
|
|
||||||
del self.callbackByMessageID[messageID]
|
|
||||||
callback(*arguments)
|
|
||||||
callbackCount = len(self.callbackByMessageID)
|
|
||||||
if self.waitingForCallbacks.is_set() and not callbackCount:
|
|
||||||
self.cancel()
|
|
||||||
|
|
||||||
def on_error(self, packetID, channelName, data):
|
def emit(self, event, *args, **kw):
|
||||||
reason, advice = data.split('+', 1)
|
path = kw.get('path', '')
|
||||||
callback = self.get_callback(channelName, 'error')
|
callback, args = find_callback(args, kw)
|
||||||
callback(reason, advice)
|
ack_id = self._set_ack_callback(callback) if callback else None
|
||||||
|
args = [event] + list(args)
|
||||||
|
socketIO_packet_type = 2
|
||||||
|
socketIO_packet_data = format_socketIO_packet_data(path, ack_id, args)
|
||||||
|
self._message(str(socketIO_packet_type) + socketIO_packet_data)
|
||||||
|
|
||||||
|
def send(self, data='', callback=None, **kw):
|
||||||
|
path = kw.get('path', '')
|
||||||
|
args = [data]
|
||||||
|
if callback:
|
||||||
|
args.append(callback)
|
||||||
|
self.emit('message', *args, path=path)
|
||||||
|
|
||||||
class RhythmicThread(Thread):
|
def _ack(self, path, ack_id, *args):
|
||||||
'Execute rhythmicFunction every few seconds'
|
socketIO_packet_type = 3
|
||||||
|
socketIO_packet_data = format_socketIO_packet_data(path, ack_id, args)
|
||||||
|
self._message(str(socketIO_packet_type) + socketIO_packet_data)
|
||||||
|
|
||||||
daemon = True
|
# React
|
||||||
|
|
||||||
def __init__(self, intervalInSeconds, rhythmicFunction, *args, **kw):
|
def wait_for_callbacks(self, seconds=None):
|
||||||
super(RhythmicThread, self).__init__()
|
self.wait(seconds, for_callbacks=True)
|
||||||
self.intervalInSeconds = intervalInSeconds
|
|
||||||
self.rhythmicFunction = rhythmicFunction
|
|
||||||
self.args = args
|
|
||||||
self.kw = kw
|
|
||||||
self.done = Event()
|
|
||||||
|
|
||||||
def run(self):
|
def _should_stop_waiting(self, for_connect=False, for_callbacks=False):
|
||||||
|
if for_connect:
|
||||||
|
for namespace in self._namespace_by_path.values():
|
||||||
|
is_namespace_connected = getattr(
|
||||||
|
namespace, '_connected', False)
|
||||||
|
if not is_namespace_connected:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
if for_callbacks and not self._has_ack_callback:
|
||||||
|
return True
|
||||||
|
return super(SocketIO, self)._should_stop_waiting()
|
||||||
|
|
||||||
|
def _process_packet(self, packet):
|
||||||
|
engineIO_packet_data = super(SocketIO, self)._process_packet(packet)
|
||||||
|
if engineIO_packet_data is None:
|
||||||
|
return
|
||||||
|
self._debug('[socket.io packet received] %s', engineIO_packet_data)
|
||||||
|
socketIO_packet_type = int(get_character(engineIO_packet_data, 0))
|
||||||
|
socketIO_packet_data = engineIO_packet_data[1:]
|
||||||
|
# Launch callbacks
|
||||||
|
path = get_namespace_path(socketIO_packet_data)
|
||||||
|
namespace = self.get_namespace(path)
|
||||||
try:
|
try:
|
||||||
while not self.done.is_set():
|
delegate = {
|
||||||
self.rhythmicFunction(*self.args, **self.kw)
|
0: self._on_connect,
|
||||||
self.done.wait(self.intervalInSeconds)
|
1: self._on_disconnect,
|
||||||
except:
|
2: self._on_event,
|
||||||
pass
|
3: self._on_ack,
|
||||||
|
4: self._on_error,
|
||||||
|
5: self._on_binary_event,
|
||||||
|
6: self._on_binary_ack,
|
||||||
|
}[socketIO_packet_type]
|
||||||
|
except KeyError:
|
||||||
|
raise PacketError(
|
||||||
|
'unexpected socket.io packet type (%s)' % socketIO_packet_type)
|
||||||
|
delegate(socketIO_packet_data, namespace)
|
||||||
|
return socketIO_packet_data
|
||||||
|
|
||||||
def cancel(self):
|
def _on_connect(self, data, namespace):
|
||||||
self.done.set()
|
namespace._connected = True
|
||||||
|
namespace._find_packet_callback('connect')()
|
||||||
|
|
||||||
|
def _on_disconnect(self, data, namespace):
|
||||||
|
namespace._connected = False
|
||||||
|
namespace._find_packet_callback('disconnect')()
|
||||||
|
|
||||||
class SocketIOError(Exception):
|
def _on_event(self, data, namespace):
|
||||||
pass
|
data_parsed = parse_socketIO_packet_data(data)
|
||||||
|
args = data_parsed.args
|
||||||
|
try:
|
||||||
|
event = args.pop(0)
|
||||||
|
except IndexError:
|
||||||
|
raise PacketError('missing event name')
|
||||||
|
if data_parsed.ack_id is not None:
|
||||||
|
args.append(self._prepare_to_send_ack(
|
||||||
|
data_parsed.path, data_parsed.ack_id))
|
||||||
|
namespace._find_packet_callback(event)(*args)
|
||||||
|
|
||||||
|
def _on_ack(self, data, namespace):
|
||||||
|
data_parsed = parse_socketIO_packet_data(data)
|
||||||
|
try:
|
||||||
|
ack_callback = self._get_ack_callback(data_parsed.ack_id)
|
||||||
|
except KeyError:
|
||||||
|
return
|
||||||
|
ack_callback(*data_parsed.args)
|
||||||
|
|
||||||
def name_callback(eventName):
|
def _on_error(self, data, namespace):
|
||||||
return 'on_' + eventName.replace(' ', '_')
|
namespace._find_packet_callback('error')(data)
|
||||||
|
|
||||||
|
def _on_binary_event(self, data, namespace):
|
||||||
|
self._warn('[not implemented] binary event')
|
||||||
|
|
||||||
|
def _on_binary_ack(self, data, namespace):
|
||||||
|
self._warn('[not implemented] binary ack')
|
||||||
|
|
||||||
|
def _prepare_to_send_ack(self, path, ack_id):
|
||||||
|
'Return function that acknowledges the server'
|
||||||
|
return lambda *args: self._ack(path, ack_id, *args)
|
||||||
|
|
||||||
|
def _set_ack_callback(self, callback):
|
||||||
|
self._ack_id += 1
|
||||||
|
self._callback_by_ack_id[self._ack_id] = callback
|
||||||
|
return self._ack_id
|
||||||
|
|
||||||
|
def _get_ack_callback(self, ack_id):
|
||||||
|
return self._callback_by_ack_id.pop(ack_id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _has_ack_callback(self):
|
||||||
|
return True if self._callback_by_ack_id else False
|
||||||
|
|
|
||||||
14
socketIO_client/exceptions.py
Normal file
14
socketIO_client/exceptions.py
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
class SocketIOError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectionError(SocketIOError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TimeoutError(SocketIOError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PacketError(SocketIOError):
|
||||||
|
pass
|
||||||
52
socketIO_client/heartbeats.py
Normal file
52
socketIO_client/heartbeats.py
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
import logging
|
||||||
|
from threading import Thread, Event
|
||||||
|
|
||||||
|
from .exceptions import ConnectionError, TimeoutError
|
||||||
|
|
||||||
|
|
||||||
|
class HeartbeatThread(Thread):
|
||||||
|
|
||||||
|
daemon = True
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, send_heartbeat,
|
||||||
|
relax_interval_in_seconds,
|
||||||
|
hurry_interval_in_seconds):
|
||||||
|
super(HeartbeatThread, self).__init__()
|
||||||
|
self._send_heartbeat = send_heartbeat
|
||||||
|
self._relax_interval_in_seconds = relax_interval_in_seconds
|
||||||
|
self._hurry_interval_in_seconds = hurry_interval_in_seconds
|
||||||
|
self._adrenaline = Event()
|
||||||
|
self._rest = Event()
|
||||||
|
self._halt = Event()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
try:
|
||||||
|
while not self._halt.is_set():
|
||||||
|
try:
|
||||||
|
self._send_heartbeat()
|
||||||
|
except TimeoutError:
|
||||||
|
pass
|
||||||
|
if self._adrenaline.is_set():
|
||||||
|
interval_in_seconds = self._hurry_interval_in_seconds
|
||||||
|
else:
|
||||||
|
interval_in_seconds = self._relax_interval_in_seconds
|
||||||
|
self._rest.wait(interval_in_seconds)
|
||||||
|
except ConnectionError:
|
||||||
|
logging.debug('[heartbeat connection error]')
|
||||||
|
|
||||||
|
def relax(self):
|
||||||
|
self._adrenaline.clear()
|
||||||
|
|
||||||
|
def hurry(self):
|
||||||
|
self._adrenaline.set()
|
||||||
|
self._rest.set()
|
||||||
|
self._rest.clear()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hurried(self):
|
||||||
|
return self._adrenaline.is_set()
|
||||||
|
|
||||||
|
def halt(self):
|
||||||
|
self._rest.set()
|
||||||
|
self._halt.set()
|
||||||
42
socketIO_client/logs.py
Normal file
42
socketIO_client/logs.py
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
class LoggingMixin(object):
|
||||||
|
|
||||||
|
def _log(self, level, msg, *attrs):
|
||||||
|
logging.log(level, '%s %s' % (self._log_name, msg), *attrs)
|
||||||
|
|
||||||
|
def _debug(self, msg, *attrs):
|
||||||
|
self._log(logging.DEBUG, msg, *attrs)
|
||||||
|
|
||||||
|
def _info(self, msg, *attrs):
|
||||||
|
self._log(logging.INFO, msg, *attrs)
|
||||||
|
|
||||||
|
def _warn(self, msg, *attrs):
|
||||||
|
self._log(logging.WARNING, msg, *attrs)
|
||||||
|
|
||||||
|
def _yield_warning_screen(self, 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
|
||||||
|
self._warn(warning)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
|
def _yield_elapsed_time(seconds=None):
|
||||||
|
start_time = time.time()
|
||||||
|
if seconds is None:
|
||||||
|
while True:
|
||||||
|
yield _get_elapsed_time(start_time)
|
||||||
|
while _get_elapsed_time(start_time) < seconds:
|
||||||
|
yield _get_elapsed_time(start_time)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_elapsed_time(start_time):
|
||||||
|
return time.time() - start_time
|
||||||
224
socketIO_client/namespaces.py
Normal file
224
socketIO_client/namespaces.py
Normal file
|
|
@ -0,0 +1,224 @@
|
||||||
|
from .logs import LoggingMixin
|
||||||
|
|
||||||
|
|
||||||
|
class EngineIONamespace(LoggingMixin):
|
||||||
|
'Define engine.io client behavior'
|
||||||
|
|
||||||
|
def __init__(self, io):
|
||||||
|
self._io = io
|
||||||
|
self._callback_by_event = {}
|
||||||
|
self._log_name = io._url
|
||||||
|
self.initialize()
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
"""Initialize custom variables here.
|
||||||
|
You can override this method."""
|
||||||
|
|
||||||
|
def on(self, event, callback):
|
||||||
|
'Define a callback to handle an event emitted by the server'
|
||||||
|
self._callback_by_event[event] = callback
|
||||||
|
|
||||||
|
def send(self, data):
|
||||||
|
'Send a message'
|
||||||
|
self._io.send(data)
|
||||||
|
|
||||||
|
def on_open(self):
|
||||||
|
"""Called after engine.io connects.
|
||||||
|
You can override this method."""
|
||||||
|
|
||||||
|
def on_close(self):
|
||||||
|
"""Called after engine.io disconnects.
|
||||||
|
You can override this method."""
|
||||||
|
|
||||||
|
def on_ping(self, data):
|
||||||
|
"""Called after engine.io sends a ping packet.
|
||||||
|
You can override this method."""
|
||||||
|
|
||||||
|
def on_pong(self, data):
|
||||||
|
"""Called after engine.io sends a pong packet.
|
||||||
|
You can override this method."""
|
||||||
|
|
||||||
|
def on_message(self, data):
|
||||||
|
"""Called after engine.io sends a message packet.
|
||||||
|
You can override this method."""
|
||||||
|
|
||||||
|
def on_upgrade(self):
|
||||||
|
"""Called after engine.io sends an upgrade packet.
|
||||||
|
You can override this method."""
|
||||||
|
|
||||||
|
def on_noop(self):
|
||||||
|
"""Called after engine.io sends a noop packet.
|
||||||
|
You can override this method."""
|
||||||
|
|
||||||
|
def _find_packet_callback(self, event):
|
||||||
|
# Check callbacks defined by on()
|
||||||
|
try:
|
||||||
|
return self._callback_by_event[event]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
# Check callbacks defined explicitly
|
||||||
|
return getattr(self, 'on_' + event)
|
||||||
|
|
||||||
|
|
||||||
|
class SocketIONamespace(EngineIONamespace):
|
||||||
|
'Define socket.io client behavior'
|
||||||
|
|
||||||
|
def __init__(self, io, path):
|
||||||
|
self.path = path
|
||||||
|
super(SocketIONamespace, self).__init__(io)
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
self._io.connect(self.path)
|
||||||
|
|
||||||
|
def disconnect(self):
|
||||||
|
self._io.disconnect(self.path)
|
||||||
|
|
||||||
|
def emit(self, event, *args, **kw):
|
||||||
|
self._io.emit(event, path=self.path, *args, **kw)
|
||||||
|
|
||||||
|
def send(self, data='', callback=None):
|
||||||
|
self._io.send(data, callback)
|
||||||
|
|
||||||
|
def on_connect(self):
|
||||||
|
"""Called after socket.io connects.
|
||||||
|
You can override this method."""
|
||||||
|
|
||||||
|
def on_reconnect(self):
|
||||||
|
"""Called after socket.io reconnects.
|
||||||
|
You can override this method."""
|
||||||
|
|
||||||
|
def on_disconnect(self):
|
||||||
|
"""Called after socket.io disconnects.
|
||||||
|
You can override this method."""
|
||||||
|
|
||||||
|
def on_event(self, event, *args):
|
||||||
|
"""
|
||||||
|
Called if there is no matching event handler.
|
||||||
|
You can override this method.
|
||||||
|
There are three ways to define an event handler:
|
||||||
|
|
||||||
|
- Call socketIO.on()
|
||||||
|
|
||||||
|
socketIO = SocketIO('localhost', 8000)
|
||||||
|
socketIO.on('my_event', my_function)
|
||||||
|
|
||||||
|
- Call namespace.on()
|
||||||
|
|
||||||
|
namespace = socketIO.get_namespace()
|
||||||
|
namespace.on('my_event', my_function)
|
||||||
|
|
||||||
|
- Define namespace.on_xxx
|
||||||
|
|
||||||
|
class Namespace(SocketIONamespace):
|
||||||
|
|
||||||
|
def on_my_event(self, *args):
|
||||||
|
my_function(*args)
|
||||||
|
|
||||||
|
socketIO.define(Namespace)"""
|
||||||
|
|
||||||
|
def on_error(self, data):
|
||||||
|
"""Called after socket.io sends an error packet.
|
||||||
|
You can override this method."""
|
||||||
|
|
||||||
|
def _find_packet_callback(self, event):
|
||||||
|
# Interpret events
|
||||||
|
if event == 'connect':
|
||||||
|
if not hasattr(self, '_was_connected'):
|
||||||
|
self._was_connected = True
|
||||||
|
else:
|
||||||
|
event = 'reconnect'
|
||||||
|
# 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 LoggingEngineIONamespace(EngineIONamespace):
|
||||||
|
|
||||||
|
def on_open(self):
|
||||||
|
self._debug('[engine.io open]')
|
||||||
|
super(LoggingEngineIONamespace, self).on_open()
|
||||||
|
|
||||||
|
def on_close(self):
|
||||||
|
self._debug('[engine.io close]')
|
||||||
|
super(LoggingEngineIONamespace, self).on_close()
|
||||||
|
|
||||||
|
def on_ping(self, data):
|
||||||
|
self._debug('[engine.io ping] %s', data)
|
||||||
|
super(LoggingEngineIONamespace, self).on_ping(data)
|
||||||
|
|
||||||
|
def on_pong(self, data):
|
||||||
|
self._debug('[engine.io pong] %s', data)
|
||||||
|
super(LoggingEngineIONamespace, self).on_pong(data)
|
||||||
|
|
||||||
|
def on_message(self, data):
|
||||||
|
self._debug('[engine.io message] %s', data)
|
||||||
|
super(LoggingEngineIONamespace, self).on_message(data)
|
||||||
|
|
||||||
|
def on_upgrade(self):
|
||||||
|
self._debug('[engine.io upgrade]')
|
||||||
|
super(LoggingEngineIONamespace, self).on_upgrade()
|
||||||
|
|
||||||
|
def on_noop(self):
|
||||||
|
self._debug('[engine.io noop]')
|
||||||
|
super(LoggingEngineIONamespace, self).on_noop()
|
||||||
|
|
||||||
|
def on_event(self, event, *args):
|
||||||
|
callback, args = find_callback(args)
|
||||||
|
arguments = [repr(_) for _ in args]
|
||||||
|
if callback:
|
||||||
|
arguments.append('callback(*args)')
|
||||||
|
self._info('[engine.io event] %s(%s)', event, ', '.join(arguments))
|
||||||
|
super(LoggingEngineIONamespace, self).on_event(event, *args)
|
||||||
|
|
||||||
|
|
||||||
|
class LoggingSocketIONamespace(SocketIONamespace, LoggingEngineIONamespace):
|
||||||
|
|
||||||
|
def on_connect(self):
|
||||||
|
self._debug(
|
||||||
|
'%s[socket.io connect]', _make_logging_header(self.path))
|
||||||
|
super(LoggingSocketIONamespace, self).on_connect()
|
||||||
|
|
||||||
|
def on_reconnect(self):
|
||||||
|
self._debug(
|
||||||
|
'%s[socket.io reconnect]', _make_logging_header(self.path))
|
||||||
|
super(LoggingSocketIONamespace, self).on_reconnect()
|
||||||
|
|
||||||
|
def on_disconnect(self):
|
||||||
|
self._debug(
|
||||||
|
'%s[socket.io disconnect]', _make_logging_header(self.path))
|
||||||
|
super(LoggingSocketIONamespace, self).on_disconnect()
|
||||||
|
|
||||||
|
def on_event(self, event, *args):
|
||||||
|
callback, args = find_callback(args)
|
||||||
|
arguments = [repr(_) for _ in args]
|
||||||
|
if callback:
|
||||||
|
arguments.append('callback(*args)')
|
||||||
|
self._info(
|
||||||
|
'%s[socket.io event] %s(%s)', _make_logging_header(self.path),
|
||||||
|
event, ', '.join(arguments))
|
||||||
|
super(LoggingSocketIONamespace, self).on_event(event, *args)
|
||||||
|
|
||||||
|
def on_error(self, data):
|
||||||
|
self._debug(
|
||||||
|
'%s[socket.io error] %s', _make_logging_header(self.path), data)
|
||||||
|
super(LoggingSocketIONamespace, self).on_error()
|
||||||
|
|
||||||
|
|
||||||
|
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 _make_logging_header(path):
|
||||||
|
return path + ' ' if path else ''
|
||||||
137
socketIO_client/parsers.py
Normal file
137
socketIO_client/parsers.py
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
import json
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
|
from .symmetries import (
|
||||||
|
decode_string, encode_string, get_byte, get_character, parse_url)
|
||||||
|
|
||||||
|
|
||||||
|
EngineIOSession = namedtuple('EngineIOSession', [
|
||||||
|
'id', 'ping_interval', 'ping_timeout', 'transport_upgrades'])
|
||||||
|
SocketIOData = namedtuple('SocketIOData', ['path', 'ack_id', 'args'])
|
||||||
|
|
||||||
|
|
||||||
|
def parse_host(host, port, resource):
|
||||||
|
if not host.startswith('http'):
|
||||||
|
host = 'http://' + host
|
||||||
|
url_pack = parse_url(host)
|
||||||
|
is_secure = url_pack.scheme == 'https'
|
||||||
|
port = port or url_pack.port or (443 if is_secure else 80)
|
||||||
|
url = '%s:%d%s/%s' % (url_pack.hostname, port, url_pack.path, resource)
|
||||||
|
return is_secure, url
|
||||||
|
|
||||||
|
|
||||||
|
def parse_engineIO_session(engineIO_packet_data):
|
||||||
|
d = json.loads(decode_string(engineIO_packet_data))
|
||||||
|
return EngineIOSession(
|
||||||
|
id=d['sid'],
|
||||||
|
ping_interval=d['pingInterval'] / float(1000),
|
||||||
|
ping_timeout=d['pingTimeout'] / float(1000),
|
||||||
|
transport_upgrades=d['upgrades'])
|
||||||
|
|
||||||
|
|
||||||
|
def encode_engineIO_content(engineIO_packets):
|
||||||
|
content = bytearray()
|
||||||
|
for packet_type, packet_data in engineIO_packets:
|
||||||
|
packet_text = format_packet_text(packet_type, packet_data)
|
||||||
|
content.extend(_make_packet_prefix(packet_text) + packet_text)
|
||||||
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
def decode_engineIO_content(content):
|
||||||
|
content_index = 0
|
||||||
|
content_length = len(content)
|
||||||
|
while content_index < content_length:
|
||||||
|
try:
|
||||||
|
content_index, packet_length = _read_packet_length(
|
||||||
|
content, content_index)
|
||||||
|
except IndexError:
|
||||||
|
break
|
||||||
|
content_index, packet_text = _read_packet_text(
|
||||||
|
content, content_index, packet_length)
|
||||||
|
engineIO_packet_type, engineIO_packet_data = parse_packet_text(
|
||||||
|
packet_text)
|
||||||
|
yield engineIO_packet_type, engineIO_packet_data
|
||||||
|
|
||||||
|
|
||||||
|
def format_socketIO_packet_data(path=None, ack_id=None, args=None):
|
||||||
|
socketIO_packet_data = json.dumps(args, ensure_ascii=False) if args else ''
|
||||||
|
if ack_id is not None:
|
||||||
|
socketIO_packet_data = str(ack_id) + socketIO_packet_data
|
||||||
|
if path:
|
||||||
|
socketIO_packet_data = path + ',' + socketIO_packet_data
|
||||||
|
return socketIO_packet_data
|
||||||
|
|
||||||
|
|
||||||
|
def parse_socketIO_packet_data(socketIO_packet_data):
|
||||||
|
data = decode_string(socketIO_packet_data)
|
||||||
|
if data.startswith('/'):
|
||||||
|
try:
|
||||||
|
path, data = data.split(',', 1)
|
||||||
|
except ValueError:
|
||||||
|
path = data
|
||||||
|
data = ''
|
||||||
|
else:
|
||||||
|
path = ''
|
||||||
|
try:
|
||||||
|
ack_id_string, data = data.split('[', 1)
|
||||||
|
data = '[' + data
|
||||||
|
ack_id = int(ack_id_string)
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
ack_id = None
|
||||||
|
try:
|
||||||
|
args = json.loads(data)
|
||||||
|
except ValueError:
|
||||||
|
args = []
|
||||||
|
return SocketIOData(path=path, ack_id=ack_id, args=args)
|
||||||
|
|
||||||
|
|
||||||
|
def format_packet_text(packet_type, packet_data):
|
||||||
|
return encode_string(str(packet_type) + packet_data)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_packet_text(packet_text):
|
||||||
|
packet_type = int(get_character(packet_text, 0))
|
||||||
|
packet_data = packet_text[1:]
|
||||||
|
return packet_type, packet_data
|
||||||
|
|
||||||
|
|
||||||
|
def get_namespace_path(socketIO_packet_data):
|
||||||
|
if not socketIO_packet_data.startswith(b'/'):
|
||||||
|
return ''
|
||||||
|
# Loop incrementally in case there is binary data
|
||||||
|
parts = []
|
||||||
|
for i in range(len(socketIO_packet_data)):
|
||||||
|
character = get_character(socketIO_packet_data, i)
|
||||||
|
if ',' == character:
|
||||||
|
break
|
||||||
|
parts.append(character)
|
||||||
|
return ''.join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_packet_prefix(packet):
|
||||||
|
length_string = str(len(packet))
|
||||||
|
header_digits = bytearray([0])
|
||||||
|
for i in range(len(length_string)):
|
||||||
|
header_digits.append(ord(length_string[i]) - 48)
|
||||||
|
header_digits.append(255)
|
||||||
|
return header_digits
|
||||||
|
|
||||||
|
|
||||||
|
def _read_packet_length(content, content_index):
|
||||||
|
while get_byte(content, content_index) != 0:
|
||||||
|
content_index += 1
|
||||||
|
content_index += 1
|
||||||
|
packet_length_string = ''
|
||||||
|
byte = get_byte(content, content_index)
|
||||||
|
while byte != 255:
|
||||||
|
packet_length_string += str(byte)
|
||||||
|
content_index += 1
|
||||||
|
byte = get_byte(content, content_index)
|
||||||
|
return content_index, int(packet_length_string)
|
||||||
|
|
||||||
|
|
||||||
|
def _read_packet_text(content, content_index, packet_length):
|
||||||
|
while get_byte(content, content_index) == 255:
|
||||||
|
content_index += 1
|
||||||
|
packet_text = content[content_index:content_index + packet_length]
|
||||||
|
return content_index + packet_length, packet_text
|
||||||
29
socketIO_client/symmetries.py
Normal file
29
socketIO_client/symmetries.py
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import six
|
||||||
|
try:
|
||||||
|
from urllib import urlencode as format_query
|
||||||
|
except ImportError:
|
||||||
|
from urllib.parse import urlencode as format_query
|
||||||
|
try:
|
||||||
|
from urlparse import urlparse as parse_url
|
||||||
|
except ImportError:
|
||||||
|
from urllib.parse import urlparse as parse_url
|
||||||
|
try:
|
||||||
|
memoryview = memoryview
|
||||||
|
except NameError:
|
||||||
|
memoryview = buffer
|
||||||
|
|
||||||
|
|
||||||
|
def get_character(x, index):
|
||||||
|
return chr(get_byte(x, index))
|
||||||
|
|
||||||
|
|
||||||
|
def get_byte(x, index):
|
||||||
|
return six.indexbytes(x, index)
|
||||||
|
|
||||||
|
|
||||||
|
def encode_string(x):
|
||||||
|
return x.encode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
def decode_string(x):
|
||||||
|
return x.decode('utf-8')
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
from socketIO_client import SocketIO, BaseNamespace
|
|
||||||
from time import sleep
|
|
||||||
from unittest import TestCase
|
|
||||||
|
|
||||||
|
|
||||||
PAYLOAD = {'bbb': 'ccc'}
|
|
||||||
ON_RESPONSE_CALLED = False
|
|
||||||
|
|
||||||
|
|
||||||
class TestSocketIO(TestCase):
|
|
||||||
|
|
||||||
def test_disconnect(self):
|
|
||||||
socketIO = SocketIO('localhost', 8000)
|
|
||||||
socketIO.disconnect()
|
|
||||||
self.assertEqual(socketIO.connected, False)
|
|
||||||
|
|
||||||
def test_emit(self):
|
|
||||||
socketIO = SocketIO('localhost', 8000, Namespace)
|
|
||||||
socketIO.emit('aaa', PAYLOAD)
|
|
||||||
sleep(0.5)
|
|
||||||
self.assertEqual(socketIO.namespace.payload, PAYLOAD)
|
|
||||||
|
|
||||||
def test_emit_with_callback(self):
|
|
||||||
global ON_RESPONSE_CALLED
|
|
||||||
ON_RESPONSE_CALLED = False
|
|
||||||
socketIO = SocketIO('localhost', 8000)
|
|
||||||
socketIO.emit('aaa', PAYLOAD, on_response)
|
|
||||||
socketIO.wait(forCallbacks=True)
|
|
||||||
self.assertEqual(ON_RESPONSE_CALLED, True)
|
|
||||||
|
|
||||||
def test_events(self):
|
|
||||||
global ON_RESPONSE_CALLED
|
|
||||||
ON_RESPONSE_CALLED = False
|
|
||||||
socketIO = SocketIO('localhost', 8000)
|
|
||||||
socketIO.on('ddd', on_response)
|
|
||||||
socketIO.emit('aaa', PAYLOAD)
|
|
||||||
sleep(0.5)
|
|
||||||
self.assertEqual(ON_RESPONSE_CALLED, True)
|
|
||||||
|
|
||||||
def test_channels(self):
|
|
||||||
mainSocket = SocketIO('localhost', 8000, Namespace)
|
|
||||||
chatSocket = mainSocket.connect('/chat', Namespace)
|
|
||||||
newsSocket = mainSocket.connect('/news', Namespace)
|
|
||||||
newsSocket.emit('aaa', PAYLOAD)
|
|
||||||
sleep(0.5)
|
|
||||||
self.assertNotEqual(mainSocket.namespace.payload, PAYLOAD)
|
|
||||||
self.assertNotEqual(chatSocket.namespace.payload, PAYLOAD)
|
|
||||||
self.assertEqual(newsSocket.namespace.payload, PAYLOAD)
|
|
||||||
|
|
||||||
|
|
||||||
class Namespace(BaseNamespace):
|
|
||||||
|
|
||||||
payload = None
|
|
||||||
|
|
||||||
def on_ddd(self, data):
|
|
||||||
self.payload = data
|
|
||||||
|
|
||||||
|
|
||||||
def on_response(*args):
|
|
||||||
global ON_RESPONSE_CALLED
|
|
||||||
ON_RESPONSE_CALLED = True
|
|
||||||
192
socketIO_client/tests/__init__.py
Normal file
192
socketIO_client/tests/__init__.py
Normal file
|
|
@ -0,0 +1,192 @@
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
from .. import SocketIO, LoggingNamespace, find_callback
|
||||||
|
|
||||||
|
|
||||||
|
HOST = 'localhost'
|
||||||
|
PORT = 9000
|
||||||
|
DATA = 'xxx'
|
||||||
|
PAYLOAD = {'xxx': 'yyy'}
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseMixin(object):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(BaseMixin, self).setUp()
|
||||||
|
self.called_on_response = False
|
||||||
|
self.wait_time_in_seconds = 1
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
super(BaseMixin, self).tearDown()
|
||||||
|
self.socketIO.disconnect()
|
||||||
|
|
||||||
|
def test_disconnect(self):
|
||||||
|
'Disconnect'
|
||||||
|
namespace = self.socketIO.define(Namespace)
|
||||||
|
self.assertTrue(self.socketIO.connected)
|
||||||
|
self.assertFalse(namespace.called_on_disconnect)
|
||||||
|
self.socketIO.disconnect()
|
||||||
|
self.assertTrue(namespace.called_on_disconnect)
|
||||||
|
self.assertFalse(self.socketIO.connected)
|
||||||
|
|
||||||
|
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_send(self):
|
||||||
|
'Send'
|
||||||
|
namespace = self.socketIO.define(Namespace)
|
||||||
|
self.socketIO.send()
|
||||||
|
self.socketIO.wait(self.wait_time_in_seconds)
|
||||||
|
self.assertEqual(namespace.response, 'message_response')
|
||||||
|
|
||||||
|
def test_send_with_data(self):
|
||||||
|
'Send with data'
|
||||||
|
namespace = self.socketIO.define(Namespace)
|
||||||
|
self.socketIO.send(DATA)
|
||||||
|
self.socketIO.wait(self.wait_time_in_seconds)
|
||||||
|
self.assertEqual(namespace.response, DATA)
|
||||||
|
|
||||||
|
def test_ack(self):
|
||||||
|
'Respond to a server callback request'
|
||||||
|
namespace = self.socketIO.define(Namespace)
|
||||||
|
self.socketIO.emit('trigger_server_expects_callback', PAYLOAD)
|
||||||
|
self.socketIO.wait(self.wait_time_in_seconds)
|
||||||
|
self.assertEqual(namespace.args_by_event, {
|
||||||
|
'server_expects_callback': (PAYLOAD,),
|
||||||
|
'server_received_callback': (PAYLOAD,),
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_wait_with_disconnect(self):
|
||||||
|
'Exit loop when the client wants to disconnect'
|
||||||
|
self.socketIO.define(Namespace)
|
||||||
|
self.socketIO.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):
|
||||||
|
'Respond to a server callback request within a namespace'
|
||||||
|
chat_namespace = self.socketIO.define(Namespace, '/chat')
|
||||||
|
chat_namespace.emit('trigger_server_expects_callback', PAYLOAD)
|
||||||
|
self.socketIO.wait(self.wait_time_in_seconds)
|
||||||
|
self.assertEqual(chat_namespace.args_by_event, {
|
||||||
|
'server_expects_callback': (PAYLOAD,),
|
||||||
|
'server_received_callback': (PAYLOAD,),
|
||||||
|
})
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
class Test_XHR_PollingTransport(BaseMixin, TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(Test_XHR_PollingTransport, self).setUp()
|
||||||
|
self.socketIO = SocketIO(HOST, PORT, LoggingNamespace, transports=[
|
||||||
|
'xhr-polling'], verify=False)
|
||||||
|
self.assertEqual(self.socketIO.transport_name, 'xhr-polling')
|
||||||
|
|
||||||
|
|
||||||
|
class Test_WebsocketTransport(BaseMixin, TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(Test_WebsocketTransport, self).setUp()
|
||||||
|
self.socketIO = SocketIO(HOST, PORT, LoggingNamespace, transports=[
|
||||||
|
'xhr-polling', 'websocket'], verify=False)
|
||||||
|
self.assertEqual(self.socketIO.transport_name, 'websocket')
|
||||||
|
|
||||||
|
|
||||||
|
class Namespace(LoggingNamespace):
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
self.called_on_disconnect = False
|
||||||
|
self.args_by_event = {}
|
||||||
|
self.response = None
|
||||||
|
|
||||||
|
def on_disconnect(self):
|
||||||
|
self.called_on_disconnect = True
|
||||||
|
|
||||||
|
def on_wait_with_disconnect_response(self):
|
||||||
|
self.disconnect()
|
||||||
|
|
||||||
|
def on_event(self, event, *args):
|
||||||
|
callback, args = find_callback(args)
|
||||||
|
if callback:
|
||||||
|
callback(*args)
|
||||||
|
self.args_by_event[event] = args
|
||||||
|
|
||||||
|
def on_message(self, data):
|
||||||
|
self.response = data
|
||||||
25
socketIO_client/tests/index.html
Normal file
25
socketIO_client/tests/index.html
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
<script src="/socket.io/socket.io.js"></script>
|
||||||
|
<script>
|
||||||
|
var socket = io('//localhost');
|
||||||
|
var chat = io('/chat');
|
||||||
|
var news = io('/news');
|
||||||
|
|
||||||
|
socket.on('server_expects_callback', function(payload, fn) {fn(payload)});
|
||||||
|
socket.emit('trigger_server_expects_callback', 'whee');
|
||||||
|
socket.emit('emit');
|
||||||
|
socket.emit('emit_with_payload');
|
||||||
|
socket.emit('emit_with_multiple_payloads', 'aaa', 'bbb');
|
||||||
|
socket.emit('emit_with_callback', function() {console.log('whee')});
|
||||||
|
socket.emit('emit_with_callback_with_payload', function(x) {console.log('whee ' + x)});
|
||||||
|
socket.emit('emit_with_callback_with_multiple_payloads', function(x, y) {console.log('whee ' + x + ' ' + y)});
|
||||||
|
socket.emit('emit_with_event');
|
||||||
|
socket.emit('aaa');
|
||||||
|
|
||||||
|
chat.on('server_expects_callback', function(payload, fn) {fn(payload)});
|
||||||
|
chat.emit('trigger_server_expects_callback', 'whee');
|
||||||
|
chat.emit('emit_with_payload');
|
||||||
|
chat.emit('aaa');
|
||||||
|
|
||||||
|
news.emit('emit_with_payload');
|
||||||
|
news.emit('aaa');
|
||||||
|
</script>
|
||||||
36
socketIO_client/tests/proxy.js
Normal file
36
socketIO_client/tests/proxy.js
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
var proxy = require('http-proxy').createProxyServer({
|
||||||
|
target: {host: 'localhost', port: 9000}
|
||||||
|
}).on('error', function(err, req, res) {
|
||||||
|
console.log('[ERROR] %s', err);
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
var server = require('http').createServer(function(req, res) {
|
||||||
|
console.log('[REQUEST.%s] %s', req.method, req.url);
|
||||||
|
console.log(req['headers']);
|
||||||
|
if (req.method == 'POST') {
|
||||||
|
var body = '';
|
||||||
|
req.on('data', function (data) {
|
||||||
|
body += data;
|
||||||
|
});
|
||||||
|
req.on('end', function () {
|
||||||
|
print_body('[REQUEST.BODY] ', body);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var write = res.write;
|
||||||
|
res.write = function(data) {
|
||||||
|
print_body('[RESPONSE.BODY] ', data);
|
||||||
|
write.call(res, data);
|
||||||
|
}
|
||||||
|
proxy.web(req, res);
|
||||||
|
});
|
||||||
|
function print_body(header, body) {
|
||||||
|
var text = String(body);
|
||||||
|
console.log(header + text);
|
||||||
|
if (text.charCodeAt(0) != 0) return;
|
||||||
|
for (var i = 0; i < text.length; i++) {
|
||||||
|
var character_code = text.charCodeAt(i);
|
||||||
|
console.log('body[%s] = %s = %s', i, text[i], character_code);
|
||||||
|
if (character_code == 65533) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
server.listen(8000);
|
||||||
98
socketIO_client/tests/serve.js
Normal file
98
socketIO_client/tests/serve.js
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
// DEBUG=* node serve.js
|
||||||
|
|
||||||
|
var argv = require('yargs').argv;
|
||||||
|
if (argv.secure) {
|
||||||
|
var fs = require('fs');
|
||||||
|
var path = require('path');
|
||||||
|
var app = require('https').createServer({
|
||||||
|
key: fs.readFileSync(path.resolve(__dirname, 'ssl.key')),
|
||||||
|
cert: fs.readFileSync(path.resolve(__dirname, 'ssl.crt'))
|
||||||
|
}, serve);
|
||||||
|
} else {
|
||||||
|
var app = require('http').createServer(serve);
|
||||||
|
}
|
||||||
|
app.listen(9000);
|
||||||
|
|
||||||
|
var io = require('socket.io')(app);
|
||||||
|
var PAYLOAD = {'xxx': 'yyy'};
|
||||||
|
|
||||||
|
io.on('connection', function(socket) {
|
||||||
|
socket.on('message', function(data, fn) {
|
||||||
|
if (fn) {
|
||||||
|
// Client requests callback
|
||||||
|
if (data) {
|
||||||
|
fn(data);
|
||||||
|
} else {
|
||||||
|
fn();
|
||||||
|
}
|
||||||
|
} else if (typeof data === 'object') {
|
||||||
|
// Data has type object or is null
|
||||||
|
socket.json.send(data ? data : 'message_response');
|
||||||
|
} else {
|
||||||
|
// Data has type string or is ''
|
||||||
|
socket.send(data ? data : 'message_response');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
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(payload1, payload2) {
|
||||||
|
socket.emit('emit_with_multiple_payloads_response', payload1, payload2);
|
||||||
|
});
|
||||||
|
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('trigger_server_expects_callback', function(payload) {
|
||||||
|
socket.emit('server_expects_callback', payload, function(payload) {
|
||||||
|
socket.emit('server_received_callback', payload);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
socket.on('aaa', function() {
|
||||||
|
socket.emit('aaa_response', PAYLOAD);
|
||||||
|
});
|
||||||
|
socket.on('bbb', function(payload, fn) {
|
||||||
|
if (fn) fn(payload);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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('trigger_server_expects_callback', function(payload) {
|
||||||
|
socket.emit('server_expects_callback', payload, function(payload) {
|
||||||
|
socket.emit('server_received_callback', payload);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function serve(req, res) {
|
||||||
|
fs.readFile(__dirname + '/index.html', function(err, data) {
|
||||||
|
res.writeHead(200);
|
||||||
|
res.end(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
21
socketIO_client/tests/ssl.crt
Normal file
21
socketIO_client/tests/ssl.crt
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDZTCCAk2gAwIBAgIJAK1HKQ8zF3cCMA0GCSqGSIb3DQEBBQUAMEkxCzAJBgNV
|
||||||
|
BAYTAlVTMQswCQYDVQQIDAJOWTELMAkGA1UEBwwCTlkxDDAKBgNVBAoMA1hZWjES
|
||||||
|
MBAGA1UEAwwJbG9jYWxob3N0MB4XDTE1MDQxNTE5NDUwNFoXDTE2MDQxNDE5NDUw
|
||||||
|
NFowSTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk5ZMQswCQYDVQQHDAJOWTEMMAoG
|
||||||
|
A1UECgwDWFlaMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUA
|
||||||
|
A4IBDwAwggEKAoIBAQDAQUM9+xbiDeJXg+7X6HgXwla2AnGKWbZ11hZZUYbQwHyq
|
||||||
|
ABDSqRQXVWvzac6b59/trZiJ7cQEH4+c8ln1C4qbCLvr1aWkL1BDAtSbFUFhQ2Sb
|
||||||
|
R/xkSUpq35yTuR5+oHgahDg1gbgXgPhB3Y6HoBlYMSpSUKF+INu354kxfYi0t4tP
|
||||||
|
8f309KUe6eQH3gXgTBR7pPJEUpaPOsrk6UR3cHCMqyzHulyfhgvkk5FN+EtSR9ex
|
||||||
|
dIrF6WXmfynhsAa/+bxbsgeBF9MNj3zvckCzxdQStdqOvy0mu40/7i9vwguh9cRo
|
||||||
|
HDn6lx5EaE+gSGU48UNnKX5iQdqEhprNVDj31MiJAgMBAAGjUDBOMB0GA1UdDgQW
|
||||||
|
BBRkFsPxYU+e6ZSFwmzoS45qiOzAaDAfBgNVHSMEGDAWgBRkFsPxYU+e6ZSFwmzo
|
||||||
|
S45qiOzAaDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQB4JyOA5bZ3
|
||||||
|
NbkMvOjpDw+tKzcNiXZIkGfoQ8NC1DbGVhgG7Ps4VjXgUB552YSUV7iwyd3G78OC
|
||||||
|
+cxEcr+BvxXHXL2Wlxy0c/ZgBjRI5VnGbYQjjI2Iy2qJV+x5IR2oZatv45soZSLq
|
||||||
|
NFCg2KpOgcSRgs0oDGVBYO0d9m73s/kOySj2NGqVJsaQXqXtLWKnqToaCfl4Vnl+
|
||||||
|
zcMdUv8ajBZEPRg6oNi2QIvcNT8fS5gd/T4OXBa7pYuC79yOZ1X6bkKsZrcAdNGM
|
||||||
|
zO/jH6jKFjIBBx1Of+uZTzfAj/eoTu3foPuUQ+Z9NNE2nkE6SLyBSlxE7wD+SfjS
|
||||||
|
4/J0PNj22Uh3
|
||||||
|
-----END CERTIFICATE-----
|
||||||
28
socketIO_client/tests/ssl.key
Normal file
28
socketIO_client/tests/ssl.key
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDAQUM9+xbiDeJX
|
||||||
|
g+7X6HgXwla2AnGKWbZ11hZZUYbQwHyqABDSqRQXVWvzac6b59/trZiJ7cQEH4+c
|
||||||
|
8ln1C4qbCLvr1aWkL1BDAtSbFUFhQ2SbR/xkSUpq35yTuR5+oHgahDg1gbgXgPhB
|
||||||
|
3Y6HoBlYMSpSUKF+INu354kxfYi0t4tP8f309KUe6eQH3gXgTBR7pPJEUpaPOsrk
|
||||||
|
6UR3cHCMqyzHulyfhgvkk5FN+EtSR9exdIrF6WXmfynhsAa/+bxbsgeBF9MNj3zv
|
||||||
|
ckCzxdQStdqOvy0mu40/7i9vwguh9cRoHDn6lx5EaE+gSGU48UNnKX5iQdqEhprN
|
||||||
|
VDj31MiJAgMBAAECggEANAFzbxC83+lhkMrfkQgRdFvdmN6QWBxsfvOql/61uUJY
|
||||||
|
dqQN6O5TwPwad33npcTTjjenS6hFndfrwUjNjLvSgp2aN/FTHVavH3FkkY7uYKEa
|
||||||
|
VebjHz20I7TZZhxtY1OFKacajV7JrZH1lduY8pccQ/8Is7ub88JvrQ+0zO5oTHnh
|
||||||
|
KEPYY5r2wLxKrzGm0NavRW9MpiHxz1vUGykvaGq9vR8dVFvZlLC5szYII+BvlII+
|
||||||
|
78XMnZbJ9ahT7dzfnzPdPPuyP3m4cdJ9c+7Advs0g2F3K/IDL3jZZCRZIaLxHIs0
|
||||||
|
PeI17teW0OmK4RWrnf6dSf0bww05x5In8GzUYgppAQKBgQD4lJVi3UmAB319CGeP
|
||||||
|
NE4cZFuMneNsuCkNEJONb8Cfsah2maM0if8tUNoR96JorjWgkUTG4oThGSQgJQw8
|
||||||
|
fPy6cW4EUhSvWCO+Q4MFFWpTcf0hBiz5O1d06FHVo39o8Ct9dv2bxJqfNtCUUf31
|
||||||
|
Fz5tvA+wvByOSazUC3AowQZ6FwKBgQDF/ksJbOBd/bu3ss7eJRjE2sXmuhfxrUiu
|
||||||
|
P5RoOEqHROAifatJk/3hwT6lx2y1g+3kJpZm9V16dNTkcuybL0yJ/VBE3uWuodrj
|
||||||
|
i9+wcg8XSnRp3BPVKzebKIKDTMdypOeb1f5yhx6cCtChRm1frKQdoXpMQqptM0jq
|
||||||
|
w3B4bryWXwKBgQCWSv+nLrPpvJ2aoyI56x3u/J59fliquw3W4FbWBOMpqnh4fJu4
|
||||||
|
gFbQRzoR8u82611xH2O9++brUhANf1jOmaMT9tDVu+rVuSyjNJ5azH/kw96PwPQg
|
||||||
|
HEjcXjpcOOYnxE4HJZJgQ5ZY/QNPKeOp88vC/RlfedyqCtF7ww6lFU+dMQKBgQC2
|
||||||
|
M7ut4sne9R8If74rZAwVLBauq1ZZi1O1NsFF33eGX/W7B9bXER+z3vfd61W4/L2x
|
||||||
|
FWmXOflaNaWsza27aZ2P5tM1bcIEIOKkQBYL9Aq7LkNPH74Ij4rOeEsStVddwy94
|
||||||
|
k0di8cFTbAhuQbdpMiCdO/qlrzvS3j0d/djEm3NlFQKBgQCpIrHaMcckCFsf2Y6o
|
||||||
|
zMnbi3859hve94OOJjauQLlw/nRE/+OaDsDN8iJoxnK0seek8ro1ixSBTScpuX8W
|
||||||
|
G2DBgqs9NrSQLe6FAckkGqVJdluoh5GewNneAcowkkauj2srnb6XtJDhFtTDY141
|
||||||
|
EPbeqGB9PUY9Ny8VzHkAb1vi6g==
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
200
socketIO_client/transports.py
Normal file
200
socketIO_client/transports.py
Normal file
|
|
@ -0,0 +1,200 @@
|
||||||
|
import requests
|
||||||
|
import six
|
||||||
|
import socket
|
||||||
|
import ssl
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import websocket
|
||||||
|
from six import string_types
|
||||||
|
|
||||||
|
from .exceptions import ConnectionError, TimeoutError
|
||||||
|
from .parsers import (
|
||||||
|
encode_engineIO_content, decode_engineIO_content,
|
||||||
|
format_packet_text, parse_packet_text)
|
||||||
|
from .symmetries import format_query, memoryview, parse_url
|
||||||
|
|
||||||
|
|
||||||
|
if not hasattr(websocket, 'create_connection'):
|
||||||
|
sys.exit("""\
|
||||||
|
An incompatible websocket library is conflicting with the one we need.
|
||||||
|
You can remove the incompatible library and install the correct one
|
||||||
|
by running the following commands:
|
||||||
|
|
||||||
|
yes | pip uninstall websocket websocket-client
|
||||||
|
pip install -U websocket-client""")
|
||||||
|
|
||||||
|
|
||||||
|
ENGINEIO_PROTOCOL = 3
|
||||||
|
TRANSPORTS = 'xhr-polling', 'websocket'
|
||||||
|
|
||||||
|
|
||||||
|
class AbstractTransport(object):
|
||||||
|
|
||||||
|
def __init__(self, http_session, is_secure, url, engineIO_session=None):
|
||||||
|
self.http_session = http_session
|
||||||
|
self.is_secure = is_secure
|
||||||
|
self.url = url
|
||||||
|
self.engineIO_session = engineIO_session
|
||||||
|
|
||||||
|
def recv_packet(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def send_packet(self, engineIO_packet_type, engineIO_packet_data=''):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def set_timeout(self, seconds=None):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class XHR_PollingTransport(AbstractTransport):
|
||||||
|
|
||||||
|
def __init__(self, http_session, is_secure, url, engineIO_session=None):
|
||||||
|
super(XHR_PollingTransport, self).__init__(
|
||||||
|
http_session, is_secure, url, engineIO_session)
|
||||||
|
self._params = {
|
||||||
|
'EIO': ENGINEIO_PROTOCOL, 'transport': 'polling'}
|
||||||
|
if engineIO_session:
|
||||||
|
self._request_index = 1
|
||||||
|
self._kw_get = dict(
|
||||||
|
timeout=engineIO_session.ping_timeout)
|
||||||
|
self._kw_post = dict(
|
||||||
|
timeout=engineIO_session.ping_timeout,
|
||||||
|
headers={'content-type': 'application/octet-stream'})
|
||||||
|
self._params['sid'] = engineIO_session.id
|
||||||
|
else:
|
||||||
|
self._request_index = 0
|
||||||
|
self._kw_get = {}
|
||||||
|
self._kw_post = {}
|
||||||
|
http_scheme = 'https' if is_secure else 'http'
|
||||||
|
self._http_url = '%s://%s/' % (http_scheme, url)
|
||||||
|
self._request_index_lock = threading.Lock()
|
||||||
|
self._send_packet_lock = threading.Lock()
|
||||||
|
|
||||||
|
def recv_packet(self):
|
||||||
|
params = dict(self._params)
|
||||||
|
params['t'] = self._get_timestamp()
|
||||||
|
response = get_response(
|
||||||
|
self.http_session.get,
|
||||||
|
self._http_url,
|
||||||
|
params=params,
|
||||||
|
**self._kw_get)
|
||||||
|
for engineIO_packet in decode_engineIO_content(response.content):
|
||||||
|
engineIO_packet_type, engineIO_packet_data = engineIO_packet
|
||||||
|
yield engineIO_packet_type, engineIO_packet_data
|
||||||
|
|
||||||
|
def send_packet(self, engineIO_packet_type, engineIO_packet_data=''):
|
||||||
|
with self._send_packet_lock:
|
||||||
|
params = dict(self._params)
|
||||||
|
params['t'] = self._get_timestamp()
|
||||||
|
data = encode_engineIO_content([
|
||||||
|
(engineIO_packet_type, engineIO_packet_data),
|
||||||
|
])
|
||||||
|
response = get_response(
|
||||||
|
self.http_session.post,
|
||||||
|
self._http_url,
|
||||||
|
params=params,
|
||||||
|
data=memoryview(data),
|
||||||
|
**self._kw_post)
|
||||||
|
assert response.content == b'ok'
|
||||||
|
|
||||||
|
def _get_timestamp(self):
|
||||||
|
with self._request_index_lock:
|
||||||
|
timestamp = '%s-%s' % (
|
||||||
|
int(time.time() * 1000), self._request_index)
|
||||||
|
self._request_index += 1
|
||||||
|
return timestamp
|
||||||
|
|
||||||
|
|
||||||
|
class WebsocketTransport(AbstractTransport):
|
||||||
|
|
||||||
|
def __init__(self, http_session, is_secure, url, engineIO_session=None):
|
||||||
|
super(WebsocketTransport, self).__init__(
|
||||||
|
http_session, is_secure, url, engineIO_session)
|
||||||
|
params = dict(http_session.params, **{
|
||||||
|
'EIO': ENGINEIO_PROTOCOL, 'transport': 'websocket'})
|
||||||
|
request = http_session.prepare_request(requests.Request('GET', url))
|
||||||
|
kw = {'header': ['%s: %s' % x for x in request.headers.items()]}
|
||||||
|
if engineIO_session:
|
||||||
|
params['sid'] = engineIO_session.id
|
||||||
|
kw['timeout'] = self._timeout = engineIO_session.ping_timeout
|
||||||
|
ws_url = '%s://%s/?%s' % (
|
||||||
|
'wss' if is_secure else 'ws', url, format_query(params))
|
||||||
|
http_scheme = 'https' if is_secure else 'http'
|
||||||
|
if http_scheme in http_session.proxies: # Use the correct proxy
|
||||||
|
proxy_url_pack = parse_url(http_session.proxies[http_scheme])
|
||||||
|
kw['http_proxy_host'] = proxy_url_pack.hostname
|
||||||
|
kw['http_proxy_port'] = proxy_url_pack.port
|
||||||
|
if proxy_url_pack.username:
|
||||||
|
kw['http_proxy_auth'] = (
|
||||||
|
proxy_url_pack.username, proxy_url_pack.password)
|
||||||
|
if http_session.verify:
|
||||||
|
if http_session.cert: # Specify certificate path on disk
|
||||||
|
if isinstance(http_session.cert, string_types):
|
||||||
|
kw['ca_certs'] = http_session.cert
|
||||||
|
else:
|
||||||
|
kw['ca_certs'] = http_session.cert[0]
|
||||||
|
else: # Do not verify the SSL certificate
|
||||||
|
kw['sslopt'] = {'cert_reqs': ssl.CERT_NONE}
|
||||||
|
try:
|
||||||
|
self._connection = websocket.create_connection(ws_url, **kw)
|
||||||
|
except Exception as e:
|
||||||
|
raise ConnectionError(e)
|
||||||
|
|
||||||
|
def recv_packet(self):
|
||||||
|
try:
|
||||||
|
packet_text = self._connection.recv()
|
||||||
|
except websocket.WebSocketTimeoutException as e:
|
||||||
|
raise TimeoutError('recv timed out (%s)' % e)
|
||||||
|
except websocket.SSLError as e:
|
||||||
|
raise ConnectionError('recv disconnected by SSL (%s)' % e)
|
||||||
|
except websocket.WebSocketConnectionClosedException as e:
|
||||||
|
raise ConnectionError('recv disconnected (%s)' % e)
|
||||||
|
except socket.error as e:
|
||||||
|
raise ConnectionError('recv disconnected (%s)' % e)
|
||||||
|
engineIO_packet_type, engineIO_packet_data = parse_packet_text(
|
||||||
|
six.b(packet_text))
|
||||||
|
yield engineIO_packet_type, engineIO_packet_data
|
||||||
|
|
||||||
|
def send_packet(self, engineIO_packet_type, engineIO_packet_data=''):
|
||||||
|
packet = format_packet_text(engineIO_packet_type, engineIO_packet_data)
|
||||||
|
try:
|
||||||
|
self._connection.send(packet)
|
||||||
|
except websocket.WebSocketTimeoutException as e:
|
||||||
|
raise TimeoutError('send timed out (%s)' % e)
|
||||||
|
except socket.error as e:
|
||||||
|
raise ConnectionError('send disconnected (%s)' % e)
|
||||||
|
except websocket.WebSocketConnectionClosedException as e:
|
||||||
|
raise ConnectionError('send disconnected (%s)' % e)
|
||||||
|
|
||||||
|
def set_timeout(self, seconds=None):
|
||||||
|
self._connection.settimeout(seconds or self._timeout)
|
||||||
|
|
||||||
|
|
||||||
|
def get_response(request, *args, **kw):
|
||||||
|
try:
|
||||||
|
response = request(*args, stream=True, **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_code = response.status_code
|
||||||
|
if 200 != status_code:
|
||||||
|
raise ConnectionError('unexpected status code (%s %s)' % (
|
||||||
|
status_code, response.text))
|
||||||
|
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', True)
|
||||||
|
http_session.cert = kw.get('cert')
|
||||||
|
http_session.cookies.update(kw.get('cookies', {}))
|
||||||
|
return http_session
|
||||||
Loading…
Add table
Add a link
Reference in a new issue