├── .gitattributes ├── .gitignore ├── .travis.yml ├── COPYING ├── MANIFEST.in ├── NOTICE ├── README.rst ├── UNLICENSE ├── requirements.testing.txt ├── requirements.txt ├── sample └── emitter.sample.py ├── setup.cfg ├── setup.py ├── socket_io_emitter ├── __init__.py └── emitter.py ├── tests └── test_emitter.py └── tox.ini /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio. 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | #PyCharm project folder 9 | .idea/ 10 | # Distribution / packaging 11 | .Python 12 | env/ 13 | bin/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # Installer logs 28 | pip-log.txt 29 | pip-delete-this-directory.txt 30 | 31 | # Unit test / coverage reports 32 | htmlcov/ 33 | .tox/ 34 | .coverage 35 | .cache 36 | nosetests.xml 37 | coverage.xml 38 | 39 | # Translations 40 | *.mo 41 | 42 | # Mr Developer 43 | .mr.developer.cfg 44 | .project 45 | .pydevproject 46 | 47 | # Rope 48 | .ropeproject 49 | 50 | # Django stuff: 51 | *.log 52 | *.pot 53 | 54 | # Sphinx documentation 55 | docs/_build/ 56 | 57 | # Virtual env 58 | virtual/ 59 | 60 | # Pytest 61 | .pytest_cache/ 62 | 63 | # VS code 64 | .vscode 65 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "2.7" 5 | - "3.3" 6 | - "3.4" 7 | - "3.5" 8 | - "3.6" 9 | - "nightly" 10 | 11 | before_script: 12 | - sudo redis-server /etc/redis/redis.conf --port 6379 13 | 14 | # command to install dependencies 15 | install: 16 | - pip install -r requirements.txt 17 | - pip install -r requirements.testing.txt 18 | - pip install -e . 19 | 20 | # command to run tests 21 | script: 22 | - py.test -s -v --cov-report xml --cov=socket_io_emitter 23 | - codecov 24 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Ziya SARIKAYA 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGES 2 | include COPYING 3 | include LICENSE 4 | include NOTICE 5 | include *.rst 6 | prune .DS_Store 7 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | NOTICE 2 | 3 | This code is made available under the MIT License and is copyright Ziya SARIKAYA. 4 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | socket.io-python-emitter 2 | ======================== 3 | 4 | .. image:: https://travis-ci.org/ziyasal/socket.io-python-emitter.svg?branch=master 5 | :target: https://travis-ci.org/ziyasal/socket.io-python-emitter 6 | 7 | .. image:: https://img.shields.io/pypi/v/socket.io-emitter.svg 8 | :target: https://pypi.org/project/socket.io-emitter/ 9 | 10 | .. image:: https://codecov.io/gh/ziyasal/socket.io-python-emitter/branch/master/graph/badge.svg 11 | :target: https://codecov.io/gh/ziyasal/socket.io-python-emitter 12 | 13 | A Python implementation of `socket.io-emitter `_. 14 | 15 | `socket.io `_ provides a hook point to easily allow you to emit events to browsers from anywhere so `socket.io-python-emitter` communicates with `socket.io `_ servers through redis. 16 | 17 | We made some changes, compatible socket.io-redis 0.2.0 and socket.io 0.1.4. 18 | 19 | How to use 20 | ---------- 21 | 22 | **Install via pip** 23 | 24 | pip install socket.io-emitter 25 | 26 | .. code-block:: python 27 | 28 | from socket_io_emitter import Emitter 29 | 30 | io=Emitter({'host': 'localhost', 'port':6379}) 31 | io.Emit('broadcast event','Hello from socket.io-python-emitter') 32 | 33 | API 34 | --- 35 | 36 | Emitter(opts) 37 | ------------- 38 | 39 | The following options are allowed: 40 | 41 | - `client`: is a `redis-py `_ compatible client 42 | This argument is optional. 43 | - `key`: the name of the key to pub/sub events on as prefix (`socket.io`) 44 | - `host`: host to connect to redis on (`localhost`) 45 | - `port`: port to connect to redis on (`6379`) 46 | 47 | If you don't want to supply a redis client object, and want 48 | `socket.io-python-emitter` to initialize one for you, make sure to supply the 49 | `host` and `port` options. 50 | 51 | Specifies a specific `room` that you want to emit to. 52 | 53 | Emitter#In(room):Emitter 54 | ------------------------ 55 | 56 | .. code-block:: python 57 | 58 | io=Emitter({'host': 'localhost', 'port':6379}) 59 | io.In("room-name").Emit("news","Hello from python emitter"); 60 | 61 | Emitter#To(room):Emitter 62 | ------------------------ 63 | 64 | .. code-block:: python 65 | 66 | io=Emitter({'host': 'localhost', 'port':6379}) 67 | 68 | io.To("room-name").Emit("news","Hello from python emitter"); 69 | 70 | We are flattening the room parameter from `[]` and `*argv`, so you can also send to several rooms like this (both examples are valid). 71 | 72 | .. code-block:: python 73 | 74 | io=Emitter({'host': 'localhost', 'port':6379}) 75 | 76 | io.To(["room1", "room2"]).Emit("news","Hello from python emitter"); 77 | io.To("room1", "room2").Emit("news","Hello from python emitter"); 78 | 79 | Emitter#Of(namespace):Emitter 80 | ----------------------------- 81 | 82 | Specifies a specific namespace that you want to emit to. 83 | 84 | .. code-block:: python 85 | 86 | io=Emitter({'host': 'localhost', 'port':6379}) 87 | 88 | io.Of("/nsp").In("room-name").Emit("news","Hello from python emitter"); 89 | 90 | Open Source Projects in Use 91 | --------------------------- 92 | 93 | * `redis-py `_ by Andy McCurdy @andymccurdy 94 | * `msgpack-python `_ by MessagePack 95 | 96 | @ziλasal & @abdullahselek 97 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /requirements.testing.txt: -------------------------------------------------------------------------------- 1 | redis 2 | pytest 3 | pytest-cov 4 | pytest-runner 5 | codecov 6 | tox 7 | tox-pyenv 8 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | msgpack-python 2 | redis 3 | -------------------------------------------------------------------------------- /sample/emitter.sample.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from emitter import Emitter 4 | 5 | 6 | __author__ = 'ziyasal' 7 | 8 | 9 | class AppWorker: 10 | def __init__(self): 11 | self.interval = 6000 * 1 12 | 13 | def start(self): 14 | io = Emitter(dict(host='localhost',port=6379,key='sio-key')) 15 | io.Emit('broadcast event', 'Hello from socket.io-emitter') 16 | threading.Timer(self.interval, self.start).start() 17 | 18 | if __name__ == '__main__': 19 | app = AppWorker() 20 | app.start() 21 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | test = pytest 3 | 4 | [check-manifest] 5 | ignore = 6 | .travis.yml 7 | violations.flake8.txt 8 | 9 | [flake8] 10 | ignore = E111,E124,E126,E221,E501 11 | 12 | [pep8] 13 | ignore = E111,E124,E126,E221,E501 14 | max-line-length = 100 15 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import absolute_import, print_function 4 | 5 | import os 6 | import re 7 | import codecs 8 | 9 | from setuptools import setup, find_packages 10 | 11 | cwd = os.path.abspath(os.path.dirname(__file__)) 12 | 13 | def read(filename): 14 | with codecs.open(os.path.join(cwd, filename), 'rb', 'utf-8') as h: 15 | return h.read() 16 | 17 | metadata = read(os.path.join(cwd, 'socket_io_emitter', '__init__.py')) 18 | 19 | def extract_metaitem(meta): 20 | meta_match = re.search(r"""^__{meta}__\s+=\s+['\"]([^'\"]*)['\"]""".format(meta=meta), 21 | metadata, re.MULTILINE) 22 | if meta_match: 23 | return meta_match.group(1) 24 | raise RuntimeError('Unable to find __{meta}__ string.'.format(meta=meta)) 25 | 26 | setup( 27 | name='socket.io-emitter', 28 | version=extract_metaitem('version'), 29 | license=extract_metaitem('license'), 30 | description=extract_metaitem('description'), 31 | long_description=(read('README.rst')), 32 | author=extract_metaitem('author'), 33 | url=extract_metaitem('url'), 34 | download_url=extract_metaitem('download_url'), 35 | packages=find_packages(exclude=('tests', 'sample')), 36 | platforms=['Any'], 37 | install_requires=['msgpack-python', 'redis'], 38 | setup_requires=['pytest-runner'], 39 | tests_require=['pytest'], 40 | keywords='socket.io, emitter, pure python module, universal module', 41 | include_package_data=True, 42 | classifiers=[ 43 | 'Intended Audience :: Developers', 44 | 'License :: OSI Approved :: MIT License', 45 | 'Operating System :: OS Independent', 46 | 'Topic :: Software Development :: Libraries :: Python Modules', 47 | 'Programming Language :: Python', 48 | 'Programming Language :: Python :: 2.7', 49 | 'Programming Language :: Python :: 3.2', 50 | 'Programming Language :: Python :: 3.3', 51 | 'Programming Language :: Python :: 3.4', 52 | 'Programming Language :: Python :: 3.5', 53 | 'Programming Language :: Python :: 3.6', 54 | ], 55 | ) 56 | -------------------------------------------------------------------------------- /socket_io_emitter/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Python implementation of socket.io-emitter.""" 4 | 5 | from __future__ import absolute_import 6 | 7 | __author__ = 'Ziya SARIKAYA' 8 | __copyright__ = 'Copyright 2014 Ziya SARIKAYA @ziyasal' 9 | __license__ = 'MIT License' 10 | __version__ = '0.1.5.1' 11 | __url__ = 'https://github.com/ziyasal/socket.io-python-emitter' 12 | __download_url__ = 'https://pypi.org/project/socket.io-emitter/' 13 | __description__ = 'Python implementation of socket.io-emitter.' 14 | 15 | from .emitter import Emitter 16 | -------------------------------------------------------------------------------- /socket_io_emitter/emitter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import redis 4 | import msgpack 5 | 6 | class Emitter: 7 | 8 | EVENT = 2 9 | BINARY_EVENT = 5 10 | 11 | def __init__(self, opts): 12 | self._opts = opts 13 | self._rooms = [] 14 | self._flags = {} 15 | 16 | self.uid = "emitter" 17 | 18 | if 'client' in self._opts and self._opts['client'] is not None: 19 | self._client = self._opts['client'] 20 | else: 21 | self._client = self._createClient() 22 | 23 | self._key = self._opts.get("key", 'socket.io') 24 | 25 | # Limit emission to a certain `room`. 26 | def In(self, *room): 27 | self._rooms.append(room) 28 | return self 29 | 30 | # Limit emission to a certain `room`. 31 | def To(self, *room): 32 | return self.In(room) 33 | 34 | # Limit emission to certain `namespace`. 35 | def Of(self, nsp): 36 | self._flags['nsp'] = nsp 37 | return self 38 | 39 | # Send the packet. 40 | def Emit(self, *args): 41 | packet = {} 42 | extras = {} 43 | 44 | packet['data'] = args 45 | packet['type'] = self.BINARY_EVENT if self._hasBin(args) else self.EVENT 46 | 47 | # set namespace to packet 48 | if 'nsp' in self._flags: 49 | packet['nsp'] = self._flags['nsp'] 50 | del self._flags['nsp'] 51 | else: 52 | packet['nsp'] = '/' 53 | 54 | extras['flags'] = self._flags if len(self._flags) > 0 else '' 55 | 56 | rooms = self._getRooms() 57 | extras['rooms'] = rooms if len(rooms) > 0 else '' 58 | 59 | if extras['rooms']: 60 | for room in rooms: 61 | chn = "#".join((self._key, packet['nsp'], room, "")) 62 | self._client.publish(chn, msgpack.packb([self.uid,packet, extras])) 63 | else: 64 | chn = "#".join((self._key, packet['nsp'], "")) 65 | self._client.publish(chn, msgpack.packb([self.uid,packet, extras])) 66 | 67 | self._flags = {} 68 | self._rooms = [] 69 | 70 | # Makes [[1,2],3,[4,[5,6]]] into an iterator of [1,2,3,4,5,6] 71 | def _flatten(self, root): 72 | if isinstance(root, (list, tuple)): 73 | for element in root: 74 | for e in self._flatten(element): 75 | yield e 76 | else: 77 | yield root 78 | 79 | # Get a list of unique rooms 80 | def _getRooms(self): 81 | return list(set(self._flatten(self._rooms))) 82 | 83 | # Not implemented yet 84 | def _hasBin(self, param): 85 | return False 86 | 87 | # Create a redis client from a `host:port` uri string. 88 | def _createClient(self): 89 | if not 'host' in self._opts: 90 | raise Exception('Missing redis `host`') 91 | if not 'port' in self._opts: 92 | raise Exception('Missing redis `port`') 93 | 94 | kwargs = { 95 | 'host': self._opts['host'], 96 | 'port': self._opts['port'], 97 | } 98 | 99 | if 'password' in self._opts: 100 | kwargs['password'] = self._opts['password'] 101 | 102 | return redis.StrictRedis(**kwargs) 103 | -------------------------------------------------------------------------------- /tests/test_emitter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from unittest import TestCase 4 | 5 | import subprocess 6 | import redis 7 | 8 | from socket_io_emitter import Emitter 9 | 10 | class TestEmitter(TestCase): 11 | 12 | @classmethod 13 | def setUpClass(cls): 14 | cls.redis_server = subprocess.Popen("redis-server", stdout=subprocess.PIPE, shell=True) 15 | 16 | def setUp(self): 17 | self.opts = dict(host='localhost', port=6379) 18 | 19 | def test_In(self): 20 | io = Emitter(self.opts) 21 | io.In('room1') 22 | self.assertEqual(len(io._rooms), 1) 23 | 24 | def test_To(self): 25 | io = Emitter(self.opts) 26 | io.To('room1') 27 | self.assertEqual(len(io._rooms), 1) 28 | 29 | def test_Of(self): 30 | io = Emitter(self.opts) 31 | io.Of('nsp') 32 | self.assertEqual(io._flags['nsp'], 'nsp') 33 | 34 | def test_Emit(self): 35 | io = Emitter(self.opts) 36 | redis_cli = subprocess.Popen('redis-cli monitor', stdout=subprocess.PIPE,stderr=subprocess.PIPE, shell=True) 37 | 38 | output = '' 39 | while True: 40 | chunk = redis_cli.stdout.read(1).decode('utf-8') 41 | if chunk == '' and redis_cli.poll() != None: 42 | break 43 | if chunk == '\n': 44 | io.Emit('broadcast event', 'Hello from socket.io-emitter') 45 | if chunk != '' and 'PUBLISH' not in output: 46 | output += chunk 47 | else: 48 | redis_cli.kill() 49 | break 50 | 51 | self.assertTrue('PUBLISH' in output) 52 | 53 | 54 | def test_Construct_Emitter_With_Client(self): 55 | client = redis.StrictRedis(host=self.opts['host'], port=self.opts['port']) 56 | io = Emitter({'client': client}) 57 | self.assertIsNotNone(io._client) 58 | 59 | def test_Construct_Emitter_With_Options(self): 60 | io = Emitter(self.opts) 61 | self.assertIsNotNone(io._client) 62 | 63 | def test_Construct_Emitter_With_Null_Client_And_Null_Options_Raises_Exception(self): 64 | self.assertRaises(Exception, Emitter, {'client': None}) 65 | 66 | @classmethod 67 | def tearDownClass(cls): 68 | if not cls.redis_server is None: 69 | cls.redis_server.kill() 70 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = clean,py27,py3,py36,pypy,pypy3 3 | skip_missing_interpreters = True 4 | 5 | [testenv] 6 | deps = -Ur{toxinidir}/requirements.txt 7 | -Ur{toxinidir}/requirements.testing.txt 8 | commands = pytest -s -v 9 | whitelist_externals = pyenv install -s 2.7.11 10 | pyenv install -s 3.6.1 11 | pyenv install -s pypy-5.3.1 12 | pyenv local 2.7.11 3.6.1 pypy-5.3.1 13 | 14 | [testenv:clean] 15 | deps = coverage 16 | commands = coverage erase 17 | 18 | [testenv:report] 19 | commands = py.test --cov-report html --cov=socket_io_emitter 20 | 21 | [testenv:coverage] 22 | commands = coverage combine 23 | coverage html 24 | coverage report 25 | codecov 26 | --------------------------------------------------------------------------------