├── .gitignore ├── .travis.yml ├── LICENSE ├── README.rst ├── autobahn_sync ├── __init__.py ├── api.py ├── callbacks_runner.py ├── core.py ├── exceptions.py ├── extensions │ ├── __init__.py │ └── flask.py ├── logger.py └── session.py ├── dev-requirements.txt ├── docs ├── Makefile ├── apireference.rst ├── conf.py ├── index.rst ├── make.bat └── tutorial.rst ├── examples ├── flask │ ├── .crossbar │ │ └── config.json │ ├── README.md │ ├── app.py │ ├── requirements.txt │ └── runserver.sh ├── multirealms │ ├── .crossbar │ │ └── config.json │ └── app.py ├── outside_twisted │ ├── .crossbar │ │ └── config.json │ └── app.py └── peewee │ ├── .crossbar │ └── config.json │ ├── README.md │ ├── app.py │ └── requirements.txt ├── setup.cfg ├── setup.py ├── tests ├── .crossbar │ └── config.json ├── conftest.py ├── fixtures.py ├── test_api.py ├── test_bad_router.py ├── test_base.py ├── test_challenge.py ├── test_flask.py ├── test_pubsub.py └── test_rpc.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | 59 | # Crossbar stuff 60 | node.key 61 | node.pid 62 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | python: 4 | - '3.4' 5 | - '2.7' 6 | install: 7 | - pip install tox-travis coveralls 8 | before_script: 9 | # Fix https://github.com/crossbario/crossbar/issues/793 by stopping using tox 10 | - pip install -U pip==8.1.1 11 | - pip install -r dev-requirements.txt 12 | - pip install pytest-cov 13 | - pip install -e . 14 | script: 15 | - flake8 . 16 | - py.test --cov=autobahn_sync 17 | after_success: 18 | - coveralls 19 | deploy: 20 | provider: pypi 21 | user: touilleMan 22 | on: 23 | tags: true 24 | repo: Scille/autobahn-sync 25 | password: 26 | secure: HC//4Gwrelt5c+7yFEDFGl09tKoOk0PJXQ165s5mYsgC4f/KOKdstxlan/fZKnAzlA3AR94yHc8JAeUlmiKG4yBhS8i9bNUlgZl2sZovfUxozYrpqdxneRiXmRqEdZH+mLXIiExTx6bAatQjCgKCeSnrwyAdVs2djL99BXBAQ1xdDfqrDdRc07tqDWcrBdCnDohuM7v8Lfz3q3oBLwmG9ffh+C9BaU65+/VIlqV0mpc56tNCg8qb1aP9enTrxDZcwuDW5u78tL2ocKhz9lY7G7h/5HWKihtwM37sYf3b0FrsJ7G8IwFKR2JIbIhWWAObm+BFCHiE3xeEn51xvNA0S+mR7DkGioL3xFJN2oTz7T3HcKnz6iYC+dffXsHaV5nUV1azHmz8j7BIIm8mMdzjQqxNvS5H+haBUqXCPOHZlZN8Vr9BPcWjiQ0vxfeWLRc7nPp3sGK1uGgkcFFLdfuaq3KDtqUGggTLxu652PdX7rYFSuxTYks+69ll8nZgcKNAQoG9VO8wgp2WYqmmSrgh08HNtPPGoUz7rt47b/+5t06VKMqZAD0/jNyrpy94+CgMoSbofcrtRHRKkIsp0IRqXz/ktF8IUf0Wa4+7Veg0FvWfotUFAJPLuRo79s7Wf047H2HDqcXszNaAfjod1mL6Dl3+SdZZWwfHKis5OtWJECw= 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Scille SAS 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 | 23 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://travis-ci.org/Scille/autobahn-sync.svg?branch=master 2 | :target: https://travis-ci.org/Scille/autobahn-sync 3 | :alt: Travis-CI 4 | 5 | .. image:: https://coveralls.io/repos/github/Scille/autobahn-sync/badge.svg?branch=master 6 | :target: https://coveralls.io/github/Scille/autobahn-sync?branch=master 7 | :alt: Code coverage 8 | 9 | .. image:: https://readthedocs.org/projects/autobahn-sync/badge/?version=latest 10 | :target: http://autobahn-sync.readthedocs.org/en/latest/?badge=latest 11 | :alt: Documentation Status 12 | 13 | Autobahn~Sync 14 | ============= 15 | 16 | `Autobahn `_ integration with `crochet `_ to provide WAMP for synchronous applications. 17 | 18 | Originaly based on the work of `Sam & Max `_ (warning: French, pr0n and awesomeness inside !). 19 | 20 | Quick example 21 | ------------- 22 | 23 | .. code-block:: python 24 | 25 | from time import sleep 26 | from autobahn_sync import publish, call, register, subscribe, run 27 | 28 | 29 | @register('com.app.shout') 30 | def shout(msg): 31 | return msg.upper() 32 | 33 | 34 | @subscribe('com.app.idea') 35 | def on_thought(msg): 36 | print("I've just had a new idea: %s" % msg) 37 | 38 | 39 | run() 40 | while True: 41 | print(call('com.app.shout', 'Autobahn is cool !')) 42 | publish('com.app.idea', 'Use autobahn everywhere !') 43 | sleep(1) 44 | 45 | 46 | This code will connect to the crossbar router (don't forget to start it 47 | before trying this snippet !) listening ``ws://127.0.0.1:8080/ws`` 48 | and register itself in realm ``realm1``. 49 | 50 | Also see the `examples `_ for more usecases 51 | 52 | Bonus 53 | ----- 54 | 55 | See `extensions `_ folder for a nice Flask extension ;-) 56 | 57 | Get it now 58 | ---------- 59 | :: 60 | 61 | pip install -U autobahn-sync 62 | 63 | License 64 | ------- 65 | 66 | MIT licensed. See the bundled `LICENSE `_ file for more details. 67 | -------------------------------------------------------------------------------- /autobahn_sync/__init__.py: -------------------------------------------------------------------------------- 1 | from .core import DEFAULT_AUTOBAHN_ROUTER, DEFAULT_AUTOBAHN_REALM, AutobahnSync 2 | from .exceptions import ( 3 | Error, SessionNotReady, SerializationError, ProtocolError, 4 | TransportLost, ApplicationError, NotAuthorized, InvalidUri, 5 | ConnectionRefusedError, 6 | AbortError, AlreadyRunningError, NotRunningError 7 | ) 8 | from .api import app, run, register, subscribe, call, publish, on_challenge 9 | 10 | 11 | __all__ = ( 12 | Error, 13 | SessionNotReady, 14 | SerializationError, 15 | ProtocolError, 16 | TransportLost, 17 | ApplicationError, 18 | NotAuthorized, 19 | InvalidUri, 20 | 21 | ConnectionRefusedError, 22 | 23 | AbortError, 24 | AlreadyRunningError, 25 | NotRunningError, 26 | 27 | DEFAULT_AUTOBAHN_ROUTER, 28 | DEFAULT_AUTOBAHN_REALM, 29 | AutobahnSync, 30 | 31 | app, 32 | run, 33 | register, 34 | subscribe, 35 | call, 36 | publish, 37 | on_challenge 38 | ) 39 | 40 | __version__ = '0.3.2' 41 | __license__ = 'MIT' 42 | -------------------------------------------------------------------------------- /autobahn_sync/api.py: -------------------------------------------------------------------------------- 1 | from .core import AutobahnSync 2 | from .exceptions import NotRunningError 3 | 4 | 5 | __all__ = ( 6 | 'app', 7 | 'run', 8 | 'register', 9 | 'subscribe', 10 | 'call', 11 | 'publish', 12 | 'on_challenge' 13 | ) 14 | 15 | 16 | app = AutobahnSync() 17 | run = app.run 18 | register = app.register 19 | subscribe = app.subscribe 20 | on_challenge = app.on_challenge 21 | 22 | 23 | def call(*args, **kwargs): 24 | if not app._started: 25 | raise NotRunningError("AutobahnSync not started, call `run` first") 26 | return app.session.call(*args, **kwargs) 27 | 28 | 29 | def publish(*args, **kwargs): 30 | if not app._started: 31 | raise NotRunningError("AutobahnSync not started, call `run` first") 32 | return app.session.publish(*args, **kwargs) 33 | -------------------------------------------------------------------------------- /autobahn_sync/callbacks_runner.py: -------------------------------------------------------------------------------- 1 | import crochet 2 | from threading import Thread 3 | from twisted.internet import reactor, defer 4 | 5 | try: 6 | from queue import Queue 7 | except ImportError: 8 | from Queue import Queue 9 | 10 | 11 | __all__ = ('CallbacksRunner', 'ThreadedCallbacksRunner') 12 | 13 | 14 | class CallbacksRunner(object): 15 | def __init__(self): 16 | self._started = False 17 | self._callbacks = Queue() 18 | 19 | def put(self, func): 20 | answer = defer.Deferred() 21 | self._callbacks.put((func, answer)) 22 | return answer 23 | 24 | def start(self): 25 | assert not self._started, 'Already started !' 26 | self._started = True 27 | while self._started: 28 | # Get back and execute requested callbacks 29 | func, answer = self._callbacks.get() 30 | try: 31 | reactor.callFromThread(answer.callback, func()) 32 | except Exception as e: 33 | reactor.callFromThread(answer.errback, e) 34 | 35 | def stop(self): 36 | self._started = False 37 | # Add dummy function to force queue wakeup 38 | self.put(lambda: None) 39 | 40 | 41 | class ThreadedCallbacksRunner(CallbacksRunner): 42 | def __init__(self): 43 | super(ThreadedCallbacksRunner, self).__init__() 44 | self._thread = None 45 | 46 | def start(self): 47 | # TODO: use twisted threadspool ? 48 | self._thread = Thread(target=super(ThreadedCallbacksRunner, self).start) 49 | self._thread.start() 50 | # Kill this thread once main have left 51 | crochet.register(self.stop) 52 | -------------------------------------------------------------------------------- /autobahn_sync/core.py: -------------------------------------------------------------------------------- 1 | import crochet 2 | from autobahn.twisted.wamp import ApplicationRunner 3 | from twisted.internet import defer, threads 4 | 5 | from .logger import logger 6 | from .exceptions import AlreadyRunningError, NotRunningError 7 | from .session import SyncSession, _AsyncSession 8 | from .callbacks_runner import CallbacksRunner, ThreadedCallbacksRunner 9 | 10 | 11 | __all__ = ('DEFAULT_AUTOBAHN_ROUTER', 'DEFAULT_AUTOBAHN_REALM', 'AutobahnSync') 12 | 13 | 14 | DEFAULT_AUTOBAHN_ROUTER = u"ws://127.0.0.1:8080/ws" 15 | DEFAULT_AUTOBAHN_REALM = u"realm1" 16 | crochet_initialized = False 17 | 18 | 19 | def _init_crochet(in_twisted=False): 20 | global crochet_initialized 21 | if crochet_initialized: 22 | return 23 | if in_twisted: 24 | crochet.no_setup() 25 | else: 26 | crochet.setup() 27 | crochet_initialized = True 28 | 29 | 30 | class AutobahnSync(object): 31 | 32 | """ 33 | Main class representing the AutobahnSync application 34 | """ 35 | 36 | def __init__(self, authmethods=None): 37 | self._authmethods = authmethods 38 | self._session = None 39 | self._async_runner = None 40 | self._async_session = None 41 | self._started = False 42 | self._callbacks_runner = None 43 | self._on_running_callbacks = [] 44 | self._on_challenge_callback = None 45 | 46 | @property 47 | def session(self): 48 | """Return the underlying :class:`session.SyncSession` 49 | object if available or raise an :class:`exceptions.NotRunningError` 50 | """ 51 | if not self._session: 52 | raise NotRunningError("No session available, is AutobahnSync running ?") 53 | return self._session 54 | 55 | def run_in_twisted(self, url=DEFAULT_AUTOBAHN_ROUTER, realm=DEFAULT_AUTOBAHN_REALM, 56 | authmethods=None, authid=None, authrole=None, authextra=None, 57 | callback=None, **kwargs): 58 | """ 59 | Start the WAMP connection. Given we cannot run synchronous stuff inside the 60 | twisted thread, use this function (which returns immediately) to do the 61 | initialization from a spawned thread. 62 | 63 | :param callback: function that will be called inside the spawned thread. 64 | Put the rest of you init (or you main loop if you have one) inside it 65 | 66 | :param authmethods: Passed to :meth:`autobahn.wamp.protocol.ApplicationSession.join` 67 | :param authid: Passed to :meth:`autobahn.wamp.protocol.ApplicationSession.join` 68 | :param authrole: Passed to :meth:`autobahn.wamp.protocol.ApplicationSession.join` 69 | :param authextra: Passed to :meth:`autobahn.wamp.protocol.ApplicationSession.join` 70 | 71 | .. note:: 72 | This function must be called instead of :meth:`AutobahnSync.run` 73 | if we are calling from twisted application (typically if we are running 74 | our application inside crossbar as a `wsgi` component) 75 | """ 76 | _init_crochet(in_twisted=True) 77 | logger.debug('run_in_crossbar, bootstraping') 78 | # No need to go non-blocking if no callback has been provided 79 | blocking = callback is None 80 | 81 | def bootstrap_and_callback(): 82 | self._bootstrap(blocking, url=url, realm=realm, 83 | authmethods=authmethods, authid=authid, authrole=authrole, 84 | authextra=authextra, **kwargs) 85 | if callback: 86 | callback() 87 | self._callbacks_runner.start() 88 | 89 | threads.deferToThread(bootstrap_and_callback) 90 | 91 | def run(self, url=DEFAULT_AUTOBAHN_ROUTER, realm=DEFAULT_AUTOBAHN_REALM, 92 | authmethods=None, authid=None, authrole=None, authextra=None, 93 | blocking=False, callback=None, **kwargs): 94 | """ 95 | Start the background twisted thread and create the WAMP connection 96 | 97 | :param blocking: If ``False`` (default) this method will spawn a new 98 | thread that will be used to run the callback events (e.i. registered and 99 | subscribed functions). If ``True`` this method will not returns and 100 | use the current thread to run the callbacks. 101 | :param callback: This callback will be called once init is done, use it 102 | with ``blocking=True`` to put your WAMP related init 103 | """ 104 | _init_crochet(in_twisted=False) 105 | self._bootstrap(blocking, url=url, realm=realm, 106 | authmethods=authmethods, authid=authid, authrole=authrole, 107 | authextra=authextra, **kwargs) 108 | if callback: 109 | callback() 110 | self._callbacks_runner.start() 111 | 112 | def stop(self): 113 | """ 114 | Terminate the WAMP session 115 | 116 | .. note:: 117 | If the :meth:`AutobahnSync.run` has been run with ``blocking=True``, 118 | it will returns then. 119 | """ 120 | if not self._started: 121 | raise NotRunningError("This AutobahnSync instance is not started") 122 | self._callbacks_runner.stop() 123 | self._started = False 124 | 125 | def _bootstrap(self, blocking, **kwargs): 126 | """Synchronous bootstrap (even if `blocking=False` is provided !) 127 | 128 | Create the WAMP session and configure the `_callbacks_runner`. 129 | """ 130 | join_config = {} 131 | for key in ('authid', 'authmethods', 'authrole', 'authextra'): 132 | val = kwargs.pop(key) 133 | if val: 134 | join_config[key] = val 135 | if self._started: 136 | raise AlreadyRunningError("This AutobahnSync instance is already started") 137 | if blocking: 138 | self._callbacks_runner = CallbacksRunner() 139 | else: 140 | self._callbacks_runner = ThreadedCallbacksRunner() 141 | 142 | @crochet.wait_for(timeout=30) 143 | def start_runner(): 144 | ready_deferred = defer.Deferred() 145 | logger.debug('[CrochetReactor] start bootstrap') 146 | 147 | def register_session(config): 148 | logger.debug('[CrochetReactor] start register_session') 149 | self._async_session = _AsyncSession(config=config, join_config=join_config) 150 | self._session = SyncSession(self._callbacks_runner, self._on_challenge_callback) 151 | self._async_session.connect_to_sync(self._session) 152 | self._session.connect_to_async(self._async_session) 153 | 154 | def resolve(result): 155 | logger.debug('[CrochetReactor] callback resolve: %s' % result) 156 | ready_deferred.callback(result) 157 | return result 158 | 159 | self._async_session.on_join_defer.addCallback(resolve) 160 | 161 | def resolve_error(failure): 162 | logger.debug('[CrochetReactor] errback resolve_error: %s' % failure) 163 | ready_deferred.errback(failure) 164 | 165 | self._async_session.on_join_defer.addErrback(resolve_error) 166 | return self._async_session 167 | 168 | self._async_runner = ApplicationRunner(**kwargs) 169 | d = self._async_runner.run(register_session, start_reactor=False) 170 | 171 | def connect_error(failure): 172 | ready_deferred.errback(failure) 173 | 174 | d.addErrback(connect_error) 175 | logger.debug('[CrochetReactor] end bootstrap') 176 | return ready_deferred 177 | 178 | logger.debug('[MainThread] call bootstrap') 179 | start_runner() 180 | logger.debug('[MainThread] call decorated register/subscribe') 181 | for cb in self._on_running_callbacks: 182 | cb() 183 | self._on_running_callbacks = [] 184 | self._started = True 185 | logger.debug('[MainThread] start callbacks runner') 186 | 187 | def register(self, procedure=None, options=None): 188 | """Decorator for the :meth:`AutobahnSync.session.register` 189 | 190 | .. note:: 191 | This decorator can be used before :meth:`AutobahnSync.run` is called. 192 | In such case the actual registration will be done at ``run()`` time. 193 | """ 194 | 195 | def decorator(func): 196 | if self._started: 197 | self.session.register(endpoint=func, procedure=procedure, options=options) 198 | else: 199 | 200 | def registerer(): 201 | self.session.register(endpoint=func, procedure=procedure, options=options) 202 | 203 | # Wait for the WAMP session to be started 204 | self._on_running_callbacks.append(registerer) 205 | return func 206 | 207 | return decorator 208 | 209 | def subscribe(self, topic, options=None): 210 | """Decorator for the :meth:`AutobahnSync.session.subscribe` 211 | 212 | .. note:: 213 | This decorator can be used before :meth:`AutobahnSync.run` is called. 214 | In such case the actual registration will be done at ``run()`` time. 215 | """ 216 | 217 | def decorator(func): 218 | if self._started: 219 | self.session.subscribe(handler=func, topic=topic, options=options) 220 | else: 221 | 222 | def subscriber(): 223 | self.session.subscribe(handler=func, topic=topic, options=options) 224 | 225 | # Wait for the WAMP session to be started 226 | self._on_running_callbacks.append(subscriber) 227 | return func 228 | 229 | return decorator 230 | 231 | def on_challenge(self, func): 232 | """Decorator providing a callback to the onChallenge event, use this 233 | instead of subclassing :meth:`autobahn.twisted.wamp.ApplicationSession.onChallenge` 234 | 235 | .. note:: 236 | This decorator can only be used before :meth:`AutobahnSync.run` is called 237 | given the ``Challenge`` event is triggered at this time 238 | """ 239 | 240 | if self._started: 241 | raise RuntimeError("Cannot register a on_challenge callback" 242 | " once the session is started") 243 | self._on_challenge_callback = func 244 | -------------------------------------------------------------------------------- /autobahn_sync/exceptions.py: -------------------------------------------------------------------------------- 1 | from autobahn.wamp.exception import ( 2 | Error, SessionNotReady, SerializationError, ProtocolError, 3 | TransportLost, ApplicationError, NotAuthorized, InvalidUri) # noqa republishing 4 | from twisted.internet.error import ConnectionRefusedError # noqa republishing 5 | 6 | 7 | __all__ = ( 8 | 'Error', 9 | 'SessionNotReady', 10 | 'SerializationError', 11 | 'ProtocolError', 12 | 'TransportLost', 13 | 'ApplicationError', 14 | 'NotAuthorized', 15 | 'InvalidUri', 16 | 17 | 'ConnectionRefusedError', 18 | 19 | 'AbortError', 20 | 'AlreadyRunningError', 21 | 'NotRunningError' 22 | ) 23 | 24 | 25 | class AbortError(Error): 26 | """ 27 | Error raised when the soutes respond with an ABORT message to our HELLO 28 | """ 29 | 30 | 31 | class AlreadyRunningError(Error): 32 | """ 33 | Error raised when trying to ``run()`` multiple time an :class:`autobahn_sync.AutobahnSync` 34 | """ 35 | 36 | 37 | class NotRunningError(Error): 38 | """ 39 | Error raised when trying to ``stop()`` multiple time an :class:`autobahn_sync.AutobahnSync` 40 | """ 41 | -------------------------------------------------------------------------------- /autobahn_sync/extensions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Scille/autobahn-sync/d75fceff0d1aee61fa6dd0168eb1cd40794ad827/autobahn_sync/extensions/__init__.py -------------------------------------------------------------------------------- /autobahn_sync/extensions/flask.py: -------------------------------------------------------------------------------- 1 | from autobahn_sync import AutobahnSync, DEFAULT_AUTOBAHN_ROUTER, DEFAULT_AUTOBAHN_REALM 2 | 3 | 4 | __all__ = ('FlaskAutobahnSync', ) 5 | 6 | 7 | class FlaskAutobahnSync(AutobahnSync): 8 | 9 | """Inherit from :class:`autobahn_sync.AutobahnSync` to integrate it with Flask. 10 | 11 | :param app: Flask app to configure, if provided :meth:`init_app` is automatically called 12 | :param config: remaining kwargs will be passed to ``init_app`` as configuration 13 | """ 14 | 15 | def __init__(self, app=None, **config): 16 | super(FlaskAutobahnSync, self).__init__() 17 | self.config = { 18 | 'router': DEFAULT_AUTOBAHN_ROUTER, 19 | 'realm': DEFAULT_AUTOBAHN_REALM, 20 | 'in_twisted': False 21 | } 22 | self.app = app 23 | if app is not None: 24 | self.init_app(app, **config) 25 | 26 | def init_app(self, app, router=None, realm=None, in_twisted=None): 27 | """Configure and call the :meth:`AutobahnSync.start` method 28 | 29 | :param app: Flask app to configure 30 | :param router: WAMP router to connect to 31 | :param realm: WAMP realm to connect to 32 | :param in_twisted: Is the code is going to run inside a Twisted application 33 | 34 | .. Note:: The config provided as argument will overwrite the one privided by ``app.config`` 35 | """ 36 | router = router or app.config.get('AUTHOBAHN_ROUTER') 37 | realm = realm or app.config.get('AUTHOBAHN_REALM') 38 | in_twisted = in_twisted or app.config.get('AUTHOBAHN_IN_TWISTED') 39 | if router: 40 | self.config['router'] = router 41 | if realm: 42 | self.config['realm'] = realm 43 | if in_twisted: 44 | self.run_in_twisted(url=self.config['router'], realm=self.config['realm']) 45 | else: 46 | self.run(url=self.config['router'], realm=self.config['realm']) 47 | -------------------------------------------------------------------------------- /autobahn_sync/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | logger = logging.getLogger('autobahn') 3 | 4 | # Time to debug ? Uncomment this ! 5 | # logger.setLevel(logging.DEBUG) 6 | # steam_handler = logging.StreamHandler() 7 | # steam_handler.setLevel(logging.DEBUG) 8 | # logger.addHandler(steam_handler) 9 | 10 | # Need twisted logs ? 11 | # import sys 12 | # from twisted.python import log 13 | # log.startLogging(sys.stdout) 14 | 15 | 16 | __all__ = ('logger', ) 17 | -------------------------------------------------------------------------------- /autobahn_sync/session.py: -------------------------------------------------------------------------------- 1 | import crochet 2 | from autobahn.wamp import message, types 3 | from autobahn.twisted.wamp import ApplicationSession 4 | from twisted.internet import defer, threads 5 | from functools import partial 6 | 7 | from .exceptions import AbortError 8 | from .logger import logger 9 | 10 | 11 | __all__ = ('_AsyncSession', 'SyncSession') 12 | 13 | 14 | class _AsyncSession(ApplicationSession): 15 | """Custom :class:`autobahn.twisted.wamp.ApplicationSession` to get 16 | notified of ABORT messages 17 | """ 18 | 19 | def __init__(self, config=None, join_config=None): 20 | super(_AsyncSession, self).__init__(config=config) 21 | self._join_config = join_config or {} 22 | self._sync_session = None 23 | self.on_join_defer = defer.Deferred() 24 | self.on_challenge_defer = defer.Deferred() 25 | 26 | def connect_to_sync(self, sync_session): 27 | self._sync_session = sync_session 28 | 29 | def onConnect(self): 30 | self.join(self.config.realm, **self._join_config) 31 | 32 | def onMessage(self, msg): 33 | if not self.is_attached(): 34 | if isinstance(msg, message.Abort): 35 | logger.debug('Received ABORT answer to our HELLO: %s' % msg) 36 | details = types.CloseDetails(msg.reason, msg.message) 37 | self.on_join_defer.errback(AbortError(details)) 38 | elif isinstance(msg, message.Welcome): 39 | logger.debug('Received WELCOME answer to our HELLO: %s' % msg) 40 | self.on_join_defer.callback(msg) 41 | else: 42 | logger.debug('Received: %s' % msg) 43 | return super(_AsyncSession, self).onMessage(msg) 44 | 45 | def onUserError(self, fail, msg): 46 | logger.error('%s\n%s' % (msg, fail.getTraceback())) 47 | super(_AsyncSession, self).onUserError(fail, msg) 48 | 49 | def onChallenge(self, challenge): 50 | logger.debug('Received CHALLENGE: %s' % challenge) 51 | # `sync_session._on_challenge` should resolve `self.on_challenge_defer` 52 | threads.deferToThread(partial(self._sync_session._on_challenge, challenge)) 53 | return self.on_challenge_defer 54 | 55 | 56 | class SyncSession(object): 57 | """Synchronous version of :class:`autobahn.twisted.wamp.ApplicationSession` 58 | """ 59 | 60 | def __init__(self, callbacks_runner, on_challenge_callback): 61 | self._async_session = None 62 | self._callbacks_runner = callbacks_runner 63 | self._on_challenge_callback = on_challenge_callback 64 | 65 | def connect_to_async(self, async_session): 66 | self._async_session = async_session 67 | 68 | @crochet.wait_for(timeout=30) 69 | def leave(self, reason=None, message=None): 70 | """Actively close this WAMP session. 71 | 72 | Replace :meth:`autobahn.wamp.interface.IApplicationSession.leave` 73 | """ 74 | # see https://github.com/crossbario/autobahn-python/issues/605 75 | return self._async_session.leave(reason=reason, log_message=message) 76 | 77 | @crochet.wait_for(timeout=30) 78 | def call(self, procedure, *args, **kwargs): 79 | """Call a remote procedure. 80 | 81 | Replace :meth:`autobahn.wamp.interface.IApplicationSession.call` 82 | """ 83 | return self._async_session.call(procedure, *args, **kwargs) 84 | 85 | @crochet.wait_for(timeout=30) 86 | def register(self, endpoint, procedure=None, options=None): 87 | """Register a procedure for remote calling. 88 | 89 | Replace :meth:`autobahn.wamp.interface.IApplicationSession.register` 90 | """ 91 | def proxy_endpoint(*args, **kwargs): 92 | return self._callbacks_runner.put(partial(endpoint, *args, **kwargs)) 93 | return self._async_session.register(proxy_endpoint, procedure=procedure, options=options) 94 | 95 | @crochet.wait_for(timeout=30) 96 | def unregister(self, registration): 97 | return registration.unregister() 98 | 99 | @crochet.wait_for(timeout=30) 100 | def publish(self, topic, *args, **kwargs): 101 | """Publish an event to a topic. 102 | 103 | Replace :meth:`autobahn.wamp.interface.IApplicationSession.publish` 104 | """ 105 | return self._async_session.publish(topic, *args, **kwargs) 106 | 107 | @crochet.wait_for(timeout=30) 108 | def subscribe(self, handler, topic=None, options=None): 109 | """Subscribe to a topic for receiving events. 110 | 111 | Replace :meth:`autobahn.wamp.interface.IApplicationSession.subscribe` 112 | """ 113 | def proxy_handler(*args, **kwargs): 114 | return self._callbacks_runner.put(partial(handler, *args, **kwargs)) 115 | return self._async_session.subscribe(proxy_handler, topic=topic, options=options) 116 | 117 | @crochet.wait_for(timeout=30) 118 | def unsubscribe(self, subscription): 119 | return subscription.unsubscribe() 120 | 121 | def _on_challenge(self, challenge): 122 | # Function actually called by async_session to do the blocking onChallenge 123 | if not self._on_challenge_callback: 124 | self._async_session.on_challenge_defer.errback( 125 | NotImplementedError('No `on_challenge` callback provided')) 126 | try: 127 | ret = self._on_challenge_callback(challenge) 128 | self._async_session.on_challenge_defer.callback(ret) 129 | except Exception as e: 130 | self._async_session.on_challenge_defer.errback(e) 131 | self._async_session.on_join_defer.errback(AbortError(e)) 132 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | crossbar 2 | flask 3 | 4 | # testing tools 5 | flake8 6 | pytest 7 | tox>=1.5.0 8 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help 23 | help: 24 | @echo "Please use \`make ' where is one of" 25 | @echo " html to make standalone HTML files" 26 | @echo " dirhtml to make HTML files named index.html in directories" 27 | @echo " singlehtml to make a single large HTML file" 28 | @echo " pickle to make pickle files" 29 | @echo " json to make JSON files" 30 | @echo " htmlhelp to make HTML files and a HTML help project" 31 | @echo " qthelp to make HTML files and a qthelp project" 32 | @echo " applehelp to make an Apple Help Book" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | @echo " coverage to run coverage check of the documentation (if enabled)" 49 | 50 | .PHONY: clean 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | .PHONY: html 55 | html: 56 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 57 | @echo 58 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 59 | 60 | .PHONY: dirhtml 61 | dirhtml: 62 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 63 | @echo 64 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 65 | 66 | .PHONY: singlehtml 67 | singlehtml: 68 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 69 | @echo 70 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 71 | 72 | .PHONY: pickle 73 | pickle: 74 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 75 | @echo 76 | @echo "Build finished; now you can process the pickle files." 77 | 78 | .PHONY: json 79 | json: 80 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 81 | @echo 82 | @echo "Build finished; now you can process the JSON files." 83 | 84 | .PHONY: htmlhelp 85 | htmlhelp: 86 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 87 | @echo 88 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 89 | ".hhp project file in $(BUILDDIR)/htmlhelp." 90 | 91 | .PHONY: qthelp 92 | qthelp: 93 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 94 | @echo 95 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 96 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 97 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Autobahn-Sync.qhcp" 98 | @echo "To view the help file:" 99 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Autobahn-Sync.qhc" 100 | 101 | .PHONY: applehelp 102 | applehelp: 103 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 104 | @echo 105 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 106 | @echo "N.B. You won't be able to view it unless you put it in" \ 107 | "~/Library/Documentation/Help or install it in your application" \ 108 | "bundle." 109 | 110 | .PHONY: devhelp 111 | devhelp: 112 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 113 | @echo 114 | @echo "Build finished." 115 | @echo "To view the help file:" 116 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Autobahn-Sync" 117 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Autobahn-Sync" 118 | @echo "# devhelp" 119 | 120 | .PHONY: epub 121 | epub: 122 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 123 | @echo 124 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 125 | 126 | .PHONY: latex 127 | latex: 128 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 129 | @echo 130 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 131 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 132 | "(use \`make latexpdf' here to do that automatically)." 133 | 134 | .PHONY: latexpdf 135 | latexpdf: 136 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 137 | @echo "Running LaTeX files through pdflatex..." 138 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 139 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 140 | 141 | .PHONY: latexpdfja 142 | latexpdfja: 143 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 144 | @echo "Running LaTeX files through platex and dvipdfmx..." 145 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 146 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 147 | 148 | .PHONY: text 149 | text: 150 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 151 | @echo 152 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 153 | 154 | .PHONY: man 155 | man: 156 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 157 | @echo 158 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 159 | 160 | .PHONY: texinfo 161 | texinfo: 162 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 163 | @echo 164 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 165 | @echo "Run \`make' in that directory to run these through makeinfo" \ 166 | "(use \`make info' here to do that automatically)." 167 | 168 | .PHONY: info 169 | info: 170 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 171 | @echo "Running Texinfo files through makeinfo..." 172 | make -C $(BUILDDIR)/texinfo info 173 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 174 | 175 | .PHONY: gettext 176 | gettext: 177 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 178 | @echo 179 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 180 | 181 | .PHONY: changes 182 | changes: 183 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 184 | @echo 185 | @echo "The overview file is in $(BUILDDIR)/changes." 186 | 187 | .PHONY: linkcheck 188 | linkcheck: 189 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 190 | @echo 191 | @echo "Link check complete; look for any errors in the above output " \ 192 | "or in $(BUILDDIR)/linkcheck/output.txt." 193 | 194 | .PHONY: doctest 195 | doctest: 196 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 197 | @echo "Testing of doctests in the sources finished, look at the " \ 198 | "results in $(BUILDDIR)/doctest/output.txt." 199 | 200 | .PHONY: coverage 201 | coverage: 202 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 203 | @echo "Testing of coverage in the sources finished, look at the " \ 204 | "results in $(BUILDDIR)/coverage/python.txt." 205 | 206 | .PHONY: xml 207 | xml: 208 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 209 | @echo 210 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 211 | 212 | .PHONY: pseudoxml 213 | pseudoxml: 214 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 215 | @echo 216 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 217 | -------------------------------------------------------------------------------- /docs/apireference.rst: -------------------------------------------------------------------------------- 1 | ============= 2 | API Reference 3 | ============= 4 | 5 | Default API 6 | ----------- 7 | 8 | Autobahn-Sync exposes an initialized :class:`autobahn_sync.AutobahnSync` at the 9 | root of the module as a quick&easy access to the API:: 10 | 11 | import autobahn_sync 12 | autobahn_sync.run(url=MY_ROUTER_URL, realm=MY_REALM) 13 | 14 | @autobahn_sync.subscribe('com.app.event') 15 | def on_event(e): 16 | print('%s happened' % e) 17 | 18 | autobahn_sync.publish('com.app.event', 'trigger !') 19 | 20 | 21 | Advanced API 22 | ------------ 23 | 24 | With the need to connect to multiple realms/routers, the default API is not enought 25 | and you should create other instances of :class:`autobahn_sync.AutobahnSync`. 26 | 27 | .. automodule:: autobahn_sync.core 28 | :members: 29 | 30 | .. automodule:: autobahn_sync.session 31 | :members: 32 | 33 | Exceptions 34 | ---------- 35 | 36 | .. note:: 37 | Most exceptions are republished from :mod:`autobahn.wamp.exception` 38 | and :mod:`twisted.internet.error` 39 | 40 | .. automodule:: autobahn_sync.exceptions 41 | :members: 42 | 43 | Flask extension 44 | --------------- 45 | 46 | Flask extension provide an easier integration of Autobahn-Sync by doing it 47 | configuration in three ways (by order of priority): 48 | - Configuration explicitly passed in ``init_app`` 49 | - Configuration present in ``app.config`` 50 | - Default configuration 51 | 52 | .. tabularcolumns:: |p{6.5cm}|p{8.5cm}| 53 | 54 | ================================= ========================================= 55 | Variable name Description 56 | ================================= ========================================= 57 | ``AUTHOBAHN_ROUTER`` WAMP router to connect to (default: ``ws://localhost:8080/ws``) 58 | ``AUTHOBAHN_REALM`` WAMP realm to connect to (default: ``realm1``) 59 | ``AUTHOBAHN_IN_TWISTED`` Set to ``true`` if the code is going to run 60 | inside a Twisted application (default: ``false``) 61 | ================================= ========================================= 62 | 63 | 64 | .. automodule:: autobahn_sync.extensions.flask 65 | :members: 66 | :undoc-members: 67 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Autobahn-Sync documentation build configuration file, created by 5 | # sphinx-quickstart on Tue Jan 26 17:36:04 2016. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | import sys 17 | import os 18 | 19 | # If extensions (or modules to document with autodoc) are in another directory, 20 | # add these directories to sys.path here. If the directory is relative to the 21 | # documentation root, use os.path.abspath to make it absolute, like shown here. 22 | sys.path.insert(0, os.path.abspath('..')) 23 | 24 | # -- General configuration ------------------------------------------------ 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | #needs_sphinx = '1.0' 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be 30 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 31 | # ones. 32 | extensions = [ 33 | 'sphinx.ext.autodoc', 34 | 'sphinx.ext.doctest', 35 | 'sphinx.ext.intersphinx', 36 | 'sphinx.ext.coverage', 37 | 'sphinx.ext.viewcode', 38 | ] 39 | 40 | # Add any paths that contain templates here, relative to this directory. 41 | templates_path = ['_templates'] 42 | 43 | # The suffix(es) of source filenames. 44 | # You can specify multiple suffix as a list of string: 45 | # source_suffix = ['.rst', '.md'] 46 | source_suffix = '.rst' 47 | 48 | # The encoding of source files. 49 | #source_encoding = 'utf-8-sig' 50 | 51 | # The master toctree document. 52 | master_doc = 'index' 53 | 54 | # General information about the project. 55 | project = 'Autobahn-Sync' 56 | copyright = '2016, Emmanuel Leblond' 57 | author = 'Emmanuel Leblond' 58 | 59 | # The version info for the project you're documenting, acts as replacement for 60 | # |version| and |release|, also used in various other places throughout the 61 | # built documents. 62 | # 63 | import autobahn_sync 64 | # The short X.Y version. 65 | version = autobahn_sync.__version__ 66 | # The full version, including alpha/beta/rc tags. 67 | release = autobahn_sync.__version__ 68 | 69 | # The language for content autogenerated by Sphinx. Refer to documentation 70 | # for a list of supported languages. 71 | # 72 | # This is also used if you do content translation via gettext catalogs. 73 | # Usually you set "language" from the command line for these cases. 74 | language = None 75 | 76 | # There are two options for replacing |today|: either, you set today to some 77 | # non-false value, then it is used: 78 | #today = '' 79 | # Else, today_fmt is used as the format for a strftime call. 80 | #today_fmt = '%B %d, %Y' 81 | 82 | # List of patterns, relative to source directory, that match files and 83 | # directories to ignore when looking for source files. 84 | exclude_patterns = ['_build'] 85 | 86 | # The reST default role (used for this markup: `text`) to use for all 87 | # documents. 88 | #default_role = None 89 | 90 | # If true, '()' will be appended to :func: etc. cross-reference text. 91 | #add_function_parentheses = True 92 | 93 | # If true, the current module name will be prepended to all description 94 | # unit titles (such as .. function::). 95 | #add_module_names = True 96 | 97 | # If true, sectionauthor and moduleauthor directives will be shown in the 98 | # output. They are ignored by default. 99 | #show_authors = False 100 | 101 | # The name of the Pygments (syntax highlighting) style to use. 102 | pygments_style = 'sphinx' 103 | 104 | # A list of ignored prefixes for module index sorting. 105 | #modindex_common_prefix = [] 106 | 107 | # If true, keep warnings as "system message" paragraphs in the built documents. 108 | #keep_warnings = False 109 | 110 | # If true, `todo` and `todoList` produce output, else they produce nothing. 111 | todo_include_todos = False 112 | 113 | 114 | # -- Options for HTML output ---------------------------------------------- 115 | 116 | # The theme to use for HTML and HTML Help pages. See the documentation for 117 | # a list of builtin themes. 118 | html_theme = 'default' 119 | 120 | # Theme options are theme-specific and customize the look and feel of a theme 121 | # further. For a list of options available for each theme, see the 122 | # documentation. 123 | #html_theme_options = {} 124 | 125 | # Add any paths that contain custom themes here, relative to this directory. 126 | #html_theme_path = [] 127 | 128 | # The name for this set of Sphinx documents. If None, it defaults to 129 | # " v documentation". 130 | #html_title = None 131 | 132 | # A shorter title for the navigation bar. Default is the same as html_title. 133 | #html_short_title = None 134 | 135 | # The name of an image file (relative to this directory) to place at the top 136 | # of the sidebar. 137 | #html_logo = None 138 | 139 | # The name of an image file (within the static path) to use as favicon of the 140 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 141 | # pixels large. 142 | #html_favicon = None 143 | 144 | # Add any paths that contain custom static files (such as style sheets) here, 145 | # relative to this directory. They are copied after the builtin static files, 146 | # so a file named "default.css" will overwrite the builtin "default.css". 147 | html_static_path = ['_static'] 148 | 149 | # Add any extra paths that contain custom files (such as robots.txt or 150 | # .htaccess) here, relative to this directory. These files are copied 151 | # directly to the root of the documentation. 152 | #html_extra_path = [] 153 | 154 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 155 | # using the given strftime format. 156 | #html_last_updated_fmt = '%b %d, %Y' 157 | 158 | # If true, SmartyPants will be used to convert quotes and dashes to 159 | # typographically correct entities. 160 | #html_use_smartypants = True 161 | 162 | # Custom sidebar templates, maps document names to template names. 163 | #html_sidebars = {} 164 | 165 | # Additional templates that should be rendered to pages, maps page names to 166 | # template names. 167 | #html_additional_pages = {} 168 | 169 | # If false, no module index is generated. 170 | #html_domain_indices = True 171 | 172 | # If false, no index is generated. 173 | #html_use_index = True 174 | 175 | # If true, the index is split into individual pages for each letter. 176 | #html_split_index = False 177 | 178 | # If true, links to the reST sources are added to the pages. 179 | #html_show_sourcelink = True 180 | 181 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 182 | #html_show_sphinx = True 183 | 184 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 185 | #html_show_copyright = True 186 | 187 | # If true, an OpenSearch description file will be output, and all pages will 188 | # contain a tag referring to it. The value of this option must be the 189 | # base URL from which the finished HTML is served. 190 | #html_use_opensearch = '' 191 | 192 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 193 | #html_file_suffix = None 194 | 195 | # Language to be used for generating the HTML full-text search index. 196 | # Sphinx supports the following languages: 197 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' 198 | # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr' 199 | #html_search_language = 'en' 200 | 201 | # A dictionary with options for the search language support, empty by default. 202 | # Now only 'ja' uses this config value 203 | #html_search_options = {'type': 'default'} 204 | 205 | # The name of a javascript file (relative to the configuration directory) that 206 | # implements a search results scorer. If empty, the default will be used. 207 | #html_search_scorer = 'scorer.js' 208 | 209 | # Output file base name for HTML help builder. 210 | htmlhelp_basename = 'Autobahn-Syncdoc' 211 | 212 | # -- Options for LaTeX output --------------------------------------------- 213 | 214 | latex_elements = { 215 | # The paper size ('letterpaper' or 'a4paper'). 216 | #'papersize': 'letterpaper', 217 | 218 | # The font size ('10pt', '11pt' or '12pt'). 219 | #'pointsize': '10pt', 220 | 221 | # Additional stuff for the LaTeX preamble. 222 | #'preamble': '', 223 | 224 | # Latex figure (float) alignment 225 | #'figure_align': 'htbp', 226 | } 227 | 228 | # Grouping the document tree into LaTeX files. List of tuples 229 | # (source start file, target name, title, 230 | # author, documentclass [howto, manual, or own class]). 231 | latex_documents = [ 232 | (master_doc, 'Autobahn-Sync.tex', 'Autobahn-Sync Documentation', 233 | 'Emmanuel Leblond', 'manual'), 234 | ] 235 | 236 | # The name of an image file (relative to this directory) to place at the top of 237 | # the title page. 238 | #latex_logo = None 239 | 240 | # For "manual" documents, if this is true, then toplevel headings are parts, 241 | # not chapters. 242 | #latex_use_parts = False 243 | 244 | # If true, show page references after internal links. 245 | #latex_show_pagerefs = False 246 | 247 | # If true, show URL addresses after external links. 248 | #latex_show_urls = False 249 | 250 | # Documents to append as an appendix to all manuals. 251 | #latex_appendices = [] 252 | 253 | # If false, no module index is generated. 254 | #latex_domain_indices = True 255 | 256 | 257 | # -- Options for manual page output --------------------------------------- 258 | 259 | # One entry per manual page. List of tuples 260 | # (source start file, name, description, authors, manual section). 261 | man_pages = [ 262 | (master_doc, 'Autobahn-Sync', 'Autobahn-Sync Documentation', 263 | [author], 1) 264 | ] 265 | 266 | # If true, show URL addresses after external links. 267 | #man_show_urls = False 268 | 269 | 270 | # -- Options for Texinfo output ------------------------------------------- 271 | 272 | # Grouping the document tree into Texinfo files. List of tuples 273 | # (source start file, target name, title, author, 274 | # dir menu entry, description, category) 275 | texinfo_documents = [ 276 | (master_doc, 'Autobahn-Sync', 'Autobahn-Sync Documentation', 277 | author, 'Autobahn-Sync', 'One line description of project.', 278 | 'Miscellaneous'), 279 | ] 280 | 281 | # Documents to append as an appendix to all manuals. 282 | #texinfo_appendices = [] 283 | 284 | # If false, no module index is generated. 285 | #texinfo_domain_indices = True 286 | 287 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 288 | #texinfo_show_urls = 'footnote' 289 | 290 | # If true, do not generate a @detailmenu in the "Top" node's menu. 291 | #texinfo_no_detailmenu = False 292 | 293 | 294 | # Example configuration for intersphinx: refer to the Python standard library. 295 | intersphinx_mapping = {'https://docs.python.org/': None} 296 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Autobahn-Sync documentation master file, created by 2 | sphinx-quickstart on Tue Jan 26 17:36:04 2016. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Autobahn-Sync's documentation ! 7 | ========================================== 8 | 9 | .. image:: https://travis-ci.org/Scille/autobahn-sync.svg?branch=master 10 | :target: https://travis-ci.org/Scille/autobahn-sync 11 | :alt: Travis-CI 12 | 13 | .. image:: https://coveralls.io/repos/github/Scille/autobahn-sync/badge.svg?branch=master 14 | :target: https://coveralls.io/github/Scille/autobahn-sync?branch=master 15 | :alt: Code coverage 16 | 17 | .. image:: https://readthedocs.org/projects/autobahn-sync/badge/?version=latest 18 | :target: http://autobahn-sync.readthedocs.org/en/latest/?badge=latest 19 | :alt: Documentation Status 20 | 21 | `Autobahn `_ integration with `crochet `_ to provide WAMP for synchronous applications. 22 | 23 | Originally based on the work of `Sam & Max `_ (warning: French, pr0n and awesomeness inside !). 24 | 25 | Contents 26 | -------- 27 | 28 | :doc:`tutorial` 29 | A quick tutorial to start with the project 30 | 31 | :doc:`apireference` 32 | The complete API documentation 33 | 34 | .. toctree:: 35 | :maxdepth: 2 36 | :numbered: 37 | :hidden: 38 | 39 | tutorial 40 | apireference 41 | 42 | Also see the `examples `_ for more usecases. 43 | 44 | Indices and tables 45 | ================== 46 | 47 | * :ref:`genindex` 48 | * :ref:`modindex` 49 | * :ref:`search` 50 | 51 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | echo. coverage to run coverage check of the documentation if enabled 41 | goto end 42 | ) 43 | 44 | if "%1" == "clean" ( 45 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 46 | del /q /s %BUILDDIR%\* 47 | goto end 48 | ) 49 | 50 | 51 | REM Check if sphinx-build is available and fallback to Python version if any 52 | %SPHINXBUILD% 1>NUL 2>NUL 53 | if errorlevel 9009 goto sphinx_python 54 | goto sphinx_ok 55 | 56 | :sphinx_python 57 | 58 | set SPHINXBUILD=python -m sphinx.__init__ 59 | %SPHINXBUILD% 2> nul 60 | if errorlevel 9009 ( 61 | echo. 62 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 63 | echo.installed, then set the SPHINXBUILD environment variable to point 64 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 65 | echo.may add the Sphinx directory to PATH. 66 | echo. 67 | echo.If you don't have Sphinx installed, grab it from 68 | echo.http://sphinx-doc.org/ 69 | exit /b 1 70 | ) 71 | 72 | :sphinx_ok 73 | 74 | 75 | if "%1" == "html" ( 76 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 80 | goto end 81 | ) 82 | 83 | if "%1" == "dirhtml" ( 84 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 88 | goto end 89 | ) 90 | 91 | if "%1" == "singlehtml" ( 92 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 93 | if errorlevel 1 exit /b 1 94 | echo. 95 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 96 | goto end 97 | ) 98 | 99 | if "%1" == "pickle" ( 100 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 101 | if errorlevel 1 exit /b 1 102 | echo. 103 | echo.Build finished; now you can process the pickle files. 104 | goto end 105 | ) 106 | 107 | if "%1" == "json" ( 108 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 109 | if errorlevel 1 exit /b 1 110 | echo. 111 | echo.Build finished; now you can process the JSON files. 112 | goto end 113 | ) 114 | 115 | if "%1" == "htmlhelp" ( 116 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 117 | if errorlevel 1 exit /b 1 118 | echo. 119 | echo.Build finished; now you can run HTML Help Workshop with the ^ 120 | .hhp project file in %BUILDDIR%/htmlhelp. 121 | goto end 122 | ) 123 | 124 | if "%1" == "qthelp" ( 125 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 129 | .qhcp project file in %BUILDDIR%/qthelp, like this: 130 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Autobahn-Sync.qhcp 131 | echo.To view the help file: 132 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Autobahn-Sync.ghc 133 | goto end 134 | ) 135 | 136 | if "%1" == "devhelp" ( 137 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. 141 | goto end 142 | ) 143 | 144 | if "%1" == "epub" ( 145 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 149 | goto end 150 | ) 151 | 152 | if "%1" == "latex" ( 153 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 157 | goto end 158 | ) 159 | 160 | if "%1" == "latexpdf" ( 161 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 162 | cd %BUILDDIR%/latex 163 | make all-pdf 164 | cd %~dp0 165 | echo. 166 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdfja" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf-ja 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "text" ( 181 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 182 | if errorlevel 1 exit /b 1 183 | echo. 184 | echo.Build finished. The text files are in %BUILDDIR%/text. 185 | goto end 186 | ) 187 | 188 | if "%1" == "man" ( 189 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 190 | if errorlevel 1 exit /b 1 191 | echo. 192 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 193 | goto end 194 | ) 195 | 196 | if "%1" == "texinfo" ( 197 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 198 | if errorlevel 1 exit /b 1 199 | echo. 200 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 201 | goto end 202 | ) 203 | 204 | if "%1" == "gettext" ( 205 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 206 | if errorlevel 1 exit /b 1 207 | echo. 208 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 209 | goto end 210 | ) 211 | 212 | if "%1" == "changes" ( 213 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 214 | if errorlevel 1 exit /b 1 215 | echo. 216 | echo.The overview file is in %BUILDDIR%/changes. 217 | goto end 218 | ) 219 | 220 | if "%1" == "linkcheck" ( 221 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 222 | if errorlevel 1 exit /b 1 223 | echo. 224 | echo.Link check complete; look for any errors in the above output ^ 225 | or in %BUILDDIR%/linkcheck/output.txt. 226 | goto end 227 | ) 228 | 229 | if "%1" == "doctest" ( 230 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 231 | if errorlevel 1 exit /b 1 232 | echo. 233 | echo.Testing of doctests in the sources finished, look at the ^ 234 | results in %BUILDDIR%/doctest/output.txt. 235 | goto end 236 | ) 237 | 238 | if "%1" == "coverage" ( 239 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 240 | if errorlevel 1 exit /b 1 241 | echo. 242 | echo.Testing of coverage in the sources finished, look at the ^ 243 | results in %BUILDDIR%/coverage/python.txt. 244 | goto end 245 | ) 246 | 247 | if "%1" == "xml" ( 248 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 249 | if errorlevel 1 exit /b 1 250 | echo. 251 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 252 | goto end 253 | ) 254 | 255 | if "%1" == "pseudoxml" ( 256 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 257 | if errorlevel 1 exit /b 1 258 | echo. 259 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 260 | goto end 261 | ) 262 | 263 | :end 264 | -------------------------------------------------------------------------------- /docs/tutorial.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Tutorial 3 | ======== 4 | 5 | Todo... 6 | -------------------------------------------------------------------------------- /examples/flask/.crossbar/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "controller": {}, 4 | "workers": [ 5 | { 6 | "type": "router", 7 | "options": { 8 | "pythonpath": [ 9 | ".." 10 | ] 11 | }, 12 | "realms": [ 13 | { 14 | "name": "realm1", 15 | "roles": [ 16 | { 17 | "name": "anonymous", 18 | "permissions": [ 19 | { 20 | "uri": "", 21 | "match": "prefix", 22 | "allow": { 23 | "call": true, 24 | "register": true, 25 | "publish": true, 26 | "subscribe": true 27 | }, 28 | "disclose": { 29 | "caller": false, 30 | "publisher": false 31 | }, 32 | "cache": true 33 | } 34 | ] 35 | } 36 | ] 37 | } 38 | ], 39 | "transports": [ 40 | { 41 | "type": "web", 42 | "endpoint": { 43 | "type": "tcp", 44 | "port": 8080 45 | }, 46 | "paths": { 47 | "/": { 48 | "type": "reverseproxy", 49 | "host": "localhost", 50 | "port": 8081 51 | }, 52 | "ws": { 53 | "type": "websocket" 54 | } 55 | } 56 | } 57 | ] 58 | } 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /examples/flask/README.md: -------------------------------------------------------------------------------- 1 | Flask extension 2 | --------------- 3 | 4 | Example make use of the flask extension to automatically configure Autobahn-Sync according to the app's config. 5 | 6 | 7 | First start crossbar 8 | 9 | ```sh 10 | $ crossbar start 11 | ``` 12 | 13 | > **Note** 14 | crossbar configuration use "reverseproxy" option which is only available 15 | in crossbar>v0.12.1, with lower version you can remove this configuration and 16 | use http://localhost:8081/ to access the example 17 | 18 | 19 | Then launch the app, to do so you have the choice between "dev mode": 20 | 21 | ```sh 22 | $ python app.py 23 | * Running on http://127.0.0.1:8081/ (Press CTRL+C to quit) 24 | * Restarting with stat 25 | * Debugger is active! 26 | * Debugger pin code: 230-774-136 27 | ``` 28 | 29 | > **Note:** 30 | Given Autobahn-Sync makes use of threads, flask's autoreload function 31 | is kind of buggy and should not be used (yeah, that sucks...) 32 | 33 | 34 | or "production mode" with Gunicorn running the app on 4 concurrent workers: 35 | 36 | ```sh 37 | $ ./runserver.sh 38 | [2016-02-24 11:28:27 +0000] [12770] [INFO] Starting gunicorn 19.4.5 39 | [2016-02-24 11:28:27 +0000] [12770] [INFO] Listening at: http://0.0.0.0:8081 (12770) 40 | [2016-02-24 11:28:27 +0000] [12770] [INFO] Using worker: sync 41 | [2016-02-24 11:28:27 +0000] [12775] [INFO] Booting worker with pid: 12775 42 | [2016-02-24 11:28:28 +0000] [12776] [INFO] Booting worker with pid: 12776 43 | [2016-02-24 11:28:28 +0000] [12784] [INFO] Booting worker with pid: 12784 44 | [2016-02-24 11:28:28 +0000] [12789] [INFO] Booting worker with pid: 12789 45 | ``` 46 | 47 | Now you can head to http://localhost:8080/ and see the request history change 48 | each time you hit reload ! 49 | -------------------------------------------------------------------------------- /examples/flask/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | from flask import Flask 3 | from datetime import datetime 4 | from autobahn_sync.extensions.flask import FlaskAutobahnSync 5 | from autobahn.wamp import PublishOptions, RegisterOptions 6 | 7 | 8 | worker_id = os.getpid() 9 | app = Flask(__name__) 10 | 11 | 12 | def bootstrap(): 13 | wamp = FlaskAutobahnSync(app) 14 | app.wamp = wamp 15 | serve_history = [] 16 | 17 | @wamp.subscribe(u'com.flask_app.page_served') 18 | def page_served(wid, timestamp): 19 | data = (wid, timestamp) 20 | print('[Worker %s] Received %s' % (worker_id, data)) 21 | serve_history.append(data) 22 | 23 | # Authorize multiple registers given we can start this app in concurrent workers 24 | register_opt = RegisterOptions(invoke=u'random') 25 | 26 | @wamp.register(u'com.flask_app.get_request_history', options=register_opt) 27 | def get_request_history(wid): 28 | print('[Worker %s] Send request history to worker %s' % (worker_id, wid)) 29 | return serve_history 30 | 31 | return app 32 | 33 | 34 | @app.route('/') 35 | def main(): 36 | timestamp = str(datetime.utcnow()) 37 | print('[Worker %s] Serve request on timestamp %s' % (worker_id, timestamp)) 38 | publish_opt = PublishOptions(exclude_me=False) 39 | app.wamp.session.publish('com.flask_app.page_served', worker_id, 40 | timestamp, options=publish_opt) 41 | serve_history = app.wamp.session.call( 42 | 'com.flask_app.get_request_history', worker_id) 43 | txts = [ 44 | "", 45 | "", 46 | "" 47 | ] 48 | txts += ["" % (wid, ts) 49 | for wid, ts in serve_history] 50 | txts.append("
Worker idRequest timestamp
%s%s
") 51 | return ''.join(txts) 52 | 53 | 54 | if __name__ == '__main__': 55 | bootstrap() 56 | app.run(port=8081, debug=True) 57 | -------------------------------------------------------------------------------- /examples/flask/requirements.txt: -------------------------------------------------------------------------------- 1 | flask 2 | -------------------------------------------------------------------------------- /examples/flask/runserver.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | PORT=8081 gunicorn "app:bootstrap()" -w 4 4 | -------------------------------------------------------------------------------- /examples/multirealms/.crossbar/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "controller": {}, 4 | "workers": [ 5 | { 6 | "type": "router", 7 | "options": { 8 | "pythonpath": [ 9 | ".." 10 | ] 11 | }, 12 | "realms": [ 13 | { 14 | "name": "realm1", 15 | "roles": [ 16 | { 17 | "name": "anonymous", 18 | "permissions": [ 19 | { 20 | "uri": "", 21 | "match": "prefix", 22 | "allow": { 23 | "call": true, 24 | "register": true, 25 | "publish": true, 26 | "subscribe": true 27 | }, 28 | "disclose": { 29 | "caller": false, 30 | "publisher": false 31 | }, 32 | "cache": true 33 | } 34 | ] 35 | } 36 | ] 37 | }, 38 | { 39 | "name": "realm2", 40 | "roles": [ 41 | { 42 | "name": "anonymous", 43 | "permissions": [ 44 | { 45 | "uri": "", 46 | "match": "prefix", 47 | "allow": { 48 | "call": true, 49 | "register": true, 50 | "publish": true, 51 | "subscribe": true 52 | }, 53 | "disclose": { 54 | "caller": false, 55 | "publisher": false 56 | }, 57 | "cache": true 58 | } 59 | ] 60 | } 61 | ] 62 | } 63 | ], 64 | "transports": [ 65 | { 66 | "type": "web", 67 | "endpoint": { 68 | "type": "tcp", 69 | "port": 8080 70 | }, 71 | "paths": { 72 | "/": { 73 | "type": "wsgi", 74 | "module": "app", 75 | "object": "app" 76 | }, 77 | "ws": { 78 | "type": "websocket" 79 | } 80 | } 81 | } 82 | ] 83 | } 84 | ] 85 | } 86 | -------------------------------------------------------------------------------- /examples/multirealms/app.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | from datetime import datetime 4 | from os import environ 5 | 6 | from flask import Flask 7 | from autobahn_sync.extensions.flask import FlaskAutobahnSync 8 | 9 | 10 | app = Flask(__name__) 11 | # Current example is run directly inside crossbar which makes use of twisted 12 | app.config['AUTHOBAHN_IN_TWISTED'] = True 13 | wamp_default = FlaskAutobahnSync(app) 14 | wamp_realm2 = FlaskAutobahnSync(app, realm=u'realm2') 15 | 16 | 17 | @wamp_default.register('com.realm1.action') 18 | def action1(): 19 | print('Got RPC on com.realm1.action') 20 | return 'realm1 %s' % datetime.utcnow() 21 | 22 | 23 | @wamp_realm2.register('com.realm2.action') 24 | def action2(): 25 | print('Got RPC on com.realm2.action') 26 | return 'realm2 %s' % datetime.utcnow() 27 | 28 | 29 | @app.route('/') 30 | def main(): 31 | print('RPC on realm1') 32 | ts1 = wamp_default.call('com.realm1.action') 33 | print('com.realm1.action returned %s' % ts1) 34 | print('RPC on realm2') 35 | ts2 = wamp_realm2.call('com.realm2.action') 36 | print('com.realm2.action returned %s' % ts2) 37 | return "realm1 RPC: %s

realm2 RPC: %s" % (ts1, ts2) 38 | 39 | 40 | if __name__ == '__main__': 41 | app.run(port=int(environ.get('PORT', 8080)), debug=False) 42 | -------------------------------------------------------------------------------- /examples/outside_twisted/.crossbar/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "controller": {}, 4 | "workers": [ 5 | { 6 | "type": "router", 7 | "options": { 8 | "pythonpath": [ 9 | ".." 10 | ] 11 | }, 12 | "realms": [ 13 | { 14 | "name": "realm1", 15 | "roles": [ 16 | { 17 | "name": "anonymous", 18 | "permissions": [ 19 | { 20 | "uri": "", 21 | "match": "prefix", 22 | "allow": { 23 | "call": true, 24 | "register": true, 25 | "publish": true, 26 | "subscribe": true 27 | }, 28 | "disclose": { 29 | "caller": false, 30 | "publisher": false 31 | }, 32 | "cache": true 33 | } 34 | ] 35 | } 36 | ] 37 | } 38 | ], 39 | "transports": [ 40 | { 41 | "type": "web", 42 | "endpoint": { 43 | "type": "tcp", 44 | "port": 8080 45 | }, 46 | "paths": { 47 | "ws": { 48 | "type": "websocket" 49 | } 50 | } 51 | } 52 | ] 53 | }, 54 | { 55 | "type": "guest", 56 | "executable": "python", 57 | "arguments": [ 58 | "-m", 59 | "app" 60 | ], 61 | "options": { 62 | "workdir": ".." 63 | } 64 | } 65 | ] 66 | } 67 | -------------------------------------------------------------------------------- /examples/outside_twisted/app.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | from datetime import datetime 4 | from os import environ 5 | 6 | from flask import Flask 7 | from autobahn_sync.extensions.flask import FlaskAutobahnSync 8 | 9 | 10 | app = Flask(__name__) 11 | app.config['AUTHOBAHN_IN_TWISTED'] = environ.get('AUTHOBAHN_IN_TWISTED', '').lower() == 'true' 12 | wamp = FlaskAutobahnSync(app) 13 | 14 | 15 | @wamp.register('com.clock.get_timestamp') 16 | def get_timestamp(): 17 | print('Got RPC on get_timestamp') 18 | return str(datetime.utcnow()) 19 | 20 | 21 | @app.route('/') 22 | def main(): 23 | wamp.publish('com.clock.connection') 24 | print('Send RPC on get_timestamp') 25 | ts = wamp.call('com.clock.get_timestamp') 26 | return 'Now is %s' % ts 27 | 28 | 29 | @wamp.subscribe('com.clock.connection') 30 | def tick_listener(): 31 | print('Received com.clock.connection') 32 | 33 | 34 | if __name__ == '__main__': 35 | app.run(port=int(environ.get('PORT', 8081)), debug=True) 36 | -------------------------------------------------------------------------------- /examples/peewee/.crossbar/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "controller": {}, 4 | "workers": [ 5 | { 6 | "type": "router", 7 | "options": { 8 | "pythonpath": [ 9 | ".." 10 | ] 11 | }, 12 | "realms": [ 13 | { 14 | "name": "realm1", 15 | "roles": [ 16 | { 17 | "name": "anonymous", 18 | "permissions": [ 19 | { 20 | "uri": "", 21 | "match": "prefix", 22 | "allow": { 23 | "call": true, 24 | "register": true, 25 | "publish": true, 26 | "subscribe": true 27 | }, 28 | "disclose": { 29 | "caller": false, 30 | "publisher": false 31 | }, 32 | "cache": true 33 | } 34 | ] 35 | } 36 | ] 37 | } 38 | ], 39 | "transports": [ 40 | { 41 | "type": "web", 42 | "endpoint": { 43 | "type": "tcp", 44 | "port": 8080 45 | }, 46 | "paths": { 47 | "ws": { 48 | "type": "websocket" 49 | } 50 | } 51 | } 52 | ] 53 | }, 54 | { 55 | "type": "guest", 56 | "executable": "python", 57 | "arguments": [ 58 | "-m", 59 | "app", 60 | "--library" 61 | ], 62 | "options": { 63 | "workdir": ".." 64 | } 65 | } 66 | ] 67 | } 68 | -------------------------------------------------------------------------------- /examples/peewee/README.md: -------------------------------------------------------------------------------- 1 | Peewee app 2 | ========== 3 | 4 | This example implement a simple app using sqlite and Peewee (don't forget to install it !) 5 | 6 | The application is divided into two parts: 7 | - a backend ``library`` that expose wamp methods to create/retreive books 8 | - a frontend ``repl`` to communicate with the user 9 | 10 | First start crossbar 11 | 12 | ```sh 13 | $ crossbar start 14 | ``` 15 | 16 | 17 | Then start the app in another terminal, given the backend part is already running 18 | with crossbar, use ``--repl`` option to only run the terminal. 19 | 20 | ```sh 21 | $ python app.py --repl 22 | Welcome to the book shell, type `help` if you're lost 23 | > 24 | ``` 25 | -------------------------------------------------------------------------------- /examples/peewee/app.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import peewee as pw 3 | import autobahn_sync 4 | 5 | 6 | try: 7 | input = raw_input # noqa 8 | except: # Except if we use Python3 9 | pass 10 | 11 | 12 | def startup_library_service(): 13 | wamp = autobahn_sync.AutobahnSync() 14 | wamp.run() 15 | db = pw.SqliteDatabase('books.db') 16 | 17 | class Book(pw.Model): 18 | title = pw.CharField() 19 | author = pw.CharField() 20 | 21 | class Meta: 22 | database = db 23 | 24 | try: 25 | db.create_table(Book) 26 | except pw.OperationalError: 27 | pass 28 | 29 | @wamp.register('com.library.get_book') 30 | def get_book(id): 31 | try: 32 | b = Book.get(id=id) 33 | except pw.DoesNotExist: 34 | return {'_error': "Doesn't exist"} 35 | return {'id': id, 'title': b.title, 'author': b.author} 36 | 37 | @wamp.register('com.library.new_book') 38 | def new_book(title, author): 39 | book = Book(title=title, author=author) 40 | book.save() 41 | wamp.session.publish('com.library.book_created', book.id) 42 | return {'id': book.id} 43 | 44 | 45 | class Repl(object): 46 | 47 | USAGE = """help: print this message 48 | new: create a book 49 | get : retrieve a book 50 | quit: leave the console""" 51 | 52 | def __init__(self): 53 | # Create another autobahn 54 | self.wamp = autobahn_sync.AutobahnSync() 55 | self.wamp.run() 56 | 57 | @self.wamp.subscribe('com.library.book_created') 58 | def on_book_created(book_id): 59 | print('[Event] Someone else has created book %s' % book_id) 60 | 61 | def get_book(self, id): 62 | result = self.wamp.session.call('com.library.get_book', id) 63 | if '_error' in result: 64 | print('Error: %s' % result['_error']) 65 | else: 66 | print('Found book %s' % result) 67 | 68 | def new_book(self): 69 | title = input('Title ? ') 70 | author = input('Author ? ') 71 | result = self.wamp.session.call('com.library.new_book', title, author) 72 | if '_error' in result: 73 | print('Error: %s' % result['_error']) 74 | else: 75 | print('Created book %s' % result['id']) 76 | 77 | def start(self): 78 | quit = False 79 | print("Welcome to the book shell, type `help` if you're lost") 80 | while not quit: 81 | cmd = input('> ') 82 | cmd = cmd.strip() 83 | if cmd == 'help': 84 | print(self.USAGE) 85 | elif cmd.startswith("new"): 86 | self.new_book() 87 | elif cmd.startswith("get"): 88 | self.get_book(cmd.split()[1:]) 89 | elif cmd == 'quit': 90 | quit = True 91 | else: 92 | print('Error: Unknow command !') 93 | 94 | 95 | if __name__ == '__main__': 96 | parser = argparse.ArgumentParser(description='Small demo of Autobahn & Peewee app') 97 | parser.add_argument('--repl', action='store_true', 98 | help='Only start the REPL loop console') 99 | parser.add_argument('--library', action='store_true', 100 | help='Only start the book storage service') 101 | args = parser.parse_args() 102 | # Check if the two arguments are not both disabled or both enabled 103 | start_both = not (args.library ^ args.repl) 104 | if args.library or start_both: 105 | startup_library_service() 106 | if not start_both: 107 | # Start infinite loop and wait 108 | print("Library lanched, hit ^C to stop.") 109 | from time import sleep 110 | while True: 111 | sleep(1) 112 | if args.repl or start_both: 113 | repl = Repl() 114 | repl.start() 115 | -------------------------------------------------------------------------------- /examples/peewee/requirements.txt: -------------------------------------------------------------------------------- 1 | peewee 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.3.2 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:setup.py] 7 | search = version='{current_version}' 8 | replace = version='{new_version}' 9 | 10 | [bumpversion:file:autobahn_sync/__init__.py] 11 | search = __version__ = '{current_version}' 12 | replace = __version__ = '{new_version}' 13 | 14 | [bdist_wheel] 15 | universal = 1 16 | 17 | [flake8] 18 | ignore = E127,E128 19 | max-line-length = 100 20 | exclude = .git,docs,tests,restkit/compat.py,env,venv,.ropeproject,_sandbox 21 | 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from setuptools import setup, find_packages 3 | 4 | 5 | with open('README.rst', 'rb') as readme_file: 6 | readme = readme_file.read().decode('utf8') 7 | 8 | 9 | REQUIRES = ( 10 | 'crochet>=1.4.0', 11 | 'autobahn>=0.12.0' 12 | ) 13 | 14 | 15 | setup( 16 | name='autobahn-sync', 17 | version='0.3.2', 18 | description='Bring autobahn to your synchronous apps !', 19 | long_description=readme, 20 | author='Emmanuel Leblond', 21 | author_email='emmanuel.leblond@gmail.com', 22 | url='https://github.com/Scille/autobahn_sync', 23 | packages=find_packages(exclude=("test*", )), 24 | package_dir={'autobahn_sync': 'autobahn_sync'}, 25 | include_package_data=True, 26 | install_requires=REQUIRES, 27 | license='MIT', 28 | zip_safe=False, 29 | keywords='autobahn autobahn.ws wamp twisted crochet flask', 30 | classifiers=[ 31 | 'Intended Audience :: Developers', 32 | 'License :: OSI Approved :: MIT License', 33 | 'Natural Language :: English', 34 | 'Programming Language :: Python :: 2', 35 | 'Programming Language :: Python :: 2.7', 36 | 'Programming Language :: Python :: 3', 37 | 'Programming Language :: Python :: 3.3', 38 | 'Programming Language :: Python :: 3.4', 39 | ], 40 | test_suite='tests', 41 | ) 42 | -------------------------------------------------------------------------------- /tests/.crossbar/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "workers": [ 4 | { 5 | "type": "router", 6 | "options": { 7 | "pythonpath": [ 8 | ".." 9 | ] 10 | }, 11 | "realms": [ 12 | { 13 | "name": "realm_limited", 14 | "roles": [ 15 | { 16 | "name": "anonymous", 17 | "permissions": [ 18 | { 19 | "uri": "pubsub.no_publish.", 20 | "match": "prefix", 21 | "allow": { 22 | "call": true, 23 | "register": true, 24 | "publish": false, 25 | "subscribe": true 26 | }, 27 | "disclose": { 28 | "caller": false, 29 | "publisher": false 30 | }, 31 | "cache": true 32 | }, 33 | { 34 | "uri": "pubsub.no_subscribe.", 35 | "match": "prefix", 36 | "allow": { 37 | "call": true, 38 | "register": true, 39 | "publish": true, 40 | "subscribe": false 41 | }, 42 | "disclose": { 43 | "caller": false, 44 | "publisher": false 45 | }, 46 | "cache": true 47 | }, 48 | { 49 | "uri": "rpc.no_call.", 50 | "match": "prefix", 51 | "allow": { 52 | "call": false, 53 | "register": true, 54 | "publish": true, 55 | "subscribe": true 56 | }, 57 | "disclose": { 58 | "caller": false, 59 | "publisher": false 60 | }, 61 | "cache": true 62 | }, 63 | { 64 | "uri": "rpc.no_register.", 65 | "match": "prefix", 66 | "allow": { 67 | "call": true, 68 | "register": false, 69 | "publish": true, 70 | "subscribe": true 71 | }, 72 | "disclose": { 73 | "caller": false, 74 | "publisher": false 75 | }, 76 | "cache": true 77 | } 78 | ] 79 | } 80 | ] 81 | }, 82 | { 83 | "name": "realm1", 84 | "roles": [ 85 | { 86 | "name": "anonymous", 87 | "permissions": [ 88 | { 89 | "uri": "", 90 | "match": "prefix", 91 | "allow": { 92 | "call": true, 93 | "register": true, 94 | "publish": true, 95 | "subscribe": true 96 | }, 97 | "disclose": { 98 | "caller": false, 99 | "publisher": false 100 | }, 101 | "cache": true 102 | } 103 | ] 104 | }, 105 | { 106 | "name": "role_from_ticket", 107 | "permissions": [ 108 | { 109 | "uri": "", 110 | "match": "prefix", 111 | "allow": { 112 | "call": true, 113 | "register": true, 114 | "publish": true, 115 | "subscribe": true 116 | }, 117 | "disclose": { 118 | "caller": false, 119 | "publisher": false 120 | }, 121 | "cache": true 122 | } 123 | ] 124 | } 125 | ], 126 | "store": { 127 | "type": "memory", 128 | "event-history": [ 129 | { 130 | "uri": "rpc.historized.event", 131 | "limit": 10000 132 | } 133 | ] 134 | } 135 | } 136 | ], 137 | "transports": [ 138 | { 139 | "type": "web", 140 | "endpoint": { 141 | "type": "tcp", 142 | "port": 8080 143 | }, 144 | "paths": { 145 | "ws": { 146 | "type": "websocket" 147 | }, 148 | "ws_auth": { 149 | "type": "websocket", 150 | "auth": { 151 | "ticket": { 152 | "type": "static", 153 | "principals": { 154 | "ticket_user_1": { 155 | "ticket": "ticket_secret", 156 | "role": "role_from_ticket" 157 | } 158 | } 159 | } 160 | } 161 | } 162 | } 163 | } 164 | ] 165 | } 166 | ] 167 | } 168 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def pytest_addoption(parser): 5 | parser.addoption("--no-router", action='store_true', 6 | help="Don't start WAMP router for the test" 7 | " (must provide one on `ws://localhost:8080/ws` then)") 8 | parser.addoption("--twisted-logs", action='store_true', help="Enable twisted logs output") 9 | 10 | 11 | def pytest_runtest_setup(item): 12 | if item.config.getoption("--twisted-logs"): 13 | import sys 14 | from twisted.python import log 15 | log.startLogging(sys.stdout) 16 | -------------------------------------------------------------------------------- /tests/fixtures.py: -------------------------------------------------------------------------------- 1 | from os import path 2 | from time import sleep 3 | import subprocess 4 | import pytest 5 | 6 | from autobahn_sync import AutobahnSync, ConnectionRefusedError 7 | 8 | 9 | CROSSBAR_CONF_DIR = path.abspath(path.dirname(__file__)) + '/.crossbar' 10 | START_CROSSBAR = not pytest.config.getoption("--no-router") 11 | 12 | 13 | @pytest.fixture(scope="module") 14 | def crossbar(request): 15 | if START_CROSSBAR: 16 | # Start a wamp router 17 | subprocess.Popen(["crossbar", "start", "--cbdir", CROSSBAR_CONF_DIR]) 18 | started = False 19 | for _ in range(20): 20 | sleep(0.5) 21 | # Try to engage a wamp connection with crossbar to make sure it is started 22 | try: 23 | test_app = AutobahnSync() 24 | test_app.run() 25 | # test_app.session.disconnect() # TODO: fix me 26 | except ConnectionRefusedError: 27 | continue 28 | else: 29 | started = True 30 | break 31 | if not started: 32 | raise RuntimeError("Couldn't connect to crossbar router") 33 | 34 | def finalizer(): 35 | p = subprocess.Popen(["crossbar", "stop", "--cbdir", CROSSBAR_CONF_DIR]) 36 | p.wait() 37 | 38 | if START_CROSSBAR: 39 | request.addfinalizer(finalizer) 40 | 41 | 42 | @pytest.fixture 43 | def wamp(crossbar): 44 | wamp = AutobahnSync() 45 | wamp.run() 46 | return wamp 47 | 48 | 49 | @pytest.fixture 50 | def wamp2(crossbar): 51 | return wamp(crossbar) 52 | -------------------------------------------------------------------------------- /tests/test_api.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from time import sleep 3 | 4 | from autobahn.wamp import PublishOptions 5 | from autobahn_sync import run, register, subscribe, publish, call, NotRunningError 6 | 7 | from fixtures import crossbar, wamp 8 | 9 | 10 | class TestApi(object): 11 | 12 | def setup(self): 13 | self.rpc_called = False 14 | self.sub_called = False 15 | 16 | def test_api(self, crossbar): 17 | publish_opt = PublishOptions(exclude_me=False) 18 | 19 | @register('api.api.rpc') 20 | def rpc(): 21 | self.rpc_called = True 22 | 23 | @subscribe('api.api.event') 24 | def sub(): 25 | self.sub_called = True 26 | 27 | with pytest.raises(NotRunningError): 28 | call('api.api.rpc') 29 | 30 | with pytest.raises(NotRunningError): 31 | publish('api.api.event', options=publish_opt) 32 | 33 | assert not self.rpc_called 34 | assert not self.sub_called 35 | 36 | run() 37 | 38 | call('api.api.rpc') 39 | publish('api.api.event', options=publish_opt) 40 | sleep(0.1) # Dirty way to wait for on_event to be called... 41 | 42 | assert self.rpc_called 43 | assert self.sub_called 44 | -------------------------------------------------------------------------------- /tests/test_bad_router.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from autobahn_sync import AutobahnSync, ConnectionRefusedError, AbortError 4 | from autobahn.wamp.exception import ApplicationError 5 | 6 | from fixtures import crossbar 7 | 8 | 9 | class TestBadRouter(object): 10 | 11 | def test_router_not_started(self): 12 | wamp = AutobahnSync() 13 | with pytest.raises(ConnectionRefusedError): 14 | wamp.run(url=u'ws://localhost:9999/missing') 15 | # Make sure we can reuse the wamp object 16 | with pytest.raises(ConnectionRefusedError): 17 | wamp.run(url=u'ws://localhost:9999/missing') 18 | 19 | def test_bad_realm(self, crossbar): 20 | wamp = AutobahnSync() 21 | with pytest.raises(AbortError) as exc: 22 | wamp.run(realm=u'bad_realm') 23 | assert str(exc.value.args[0]) == 'CloseDetails(reason=, message=\'no realm "bad_realm" exists on this router\')' 24 | -------------------------------------------------------------------------------- /tests/test_base.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from time import sleep 3 | 4 | from autobahn_sync import ( 5 | AutobahnSync, ConnectionRefusedError, NotRunningError, 6 | AlreadyRunningError, TransportLost) 7 | 8 | from fixtures import crossbar, wamp 9 | 10 | 11 | class Test(object): 12 | 13 | def test_connect(self, crossbar): 14 | wamp = AutobahnSync() 15 | wamp.run() 16 | 17 | def test_get_session(self, crossbar): 18 | wamp = AutobahnSync() 19 | with pytest.raises(NotRunningError) as exc: 20 | wamp.session 21 | assert str(exc.value.args[0]) == 'No session available, is AutobahnSync running ?' 22 | 23 | def test_already_running(self, crossbar): 24 | wamp = AutobahnSync() 25 | wamp.run() 26 | with pytest.raises(AlreadyRunningError): 27 | wamp.run() 28 | 29 | def test_leave(self, crossbar): 30 | wamp = AutobahnSync() 31 | wamp.run() 32 | wamp.session.publish('com.disconnect.ready') 33 | wamp.session.leave() 34 | with pytest.raises(TransportLost): 35 | wamp.session.publish('com.disconnect.no_realm') 36 | 37 | def test_stop(self, crossbar): 38 | wamp = AutobahnSync() 39 | wamp.run() 40 | wamp.session.publish('com.disconnect.ready') 41 | wamp.session.leave() 42 | wamp.stop() 43 | sleep(0.1) # Dirty way to wait for stop... 44 | with pytest.raises(TransportLost): 45 | wamp.session.publish('com.disconnect.no_realm') 46 | with pytest.raises(NotRunningError) as exc: 47 | wamp.stop() 48 | assert str(exc.value.args[0]) == "This AutobahnSync instance is not started" -------------------------------------------------------------------------------- /tests/test_challenge.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from time import sleep 3 | 4 | from autobahn_sync import (AbortError, 5 | AutobahnSync, ConnectionRefusedError, NotRunningError, 6 | AlreadyRunningError, TransportLost) 7 | 8 | from fixtures import crossbar, wamp 9 | 10 | 11 | class TestChallenge(object): 12 | 13 | def test_no_auth_method(self, crossbar): 14 | wamp = AutobahnSync() 15 | with pytest.raises(AbortError) as exc: 16 | wamp.run(url=u'ws://localhost:8080/ws_auth', authmethods=[u'wampcra']) 17 | assert str(exc.value.args[0].reason) == 'wamp.error.no_auth_method' 18 | 19 | def test_bad_authid(self, crossbar): 20 | wamp = AutobahnSync() 21 | 22 | @wamp.on_challenge 23 | def on_challenge(challenge): 24 | pass 25 | 26 | with pytest.raises(AbortError) as exc: 27 | wamp.run(url=u'ws://localhost:8080/ws_auth', authid='dummy', authmethods=[u'ticket']) 28 | assert str(exc.value.args[0].reason) == 'wamp.error.not_authorized' 29 | 30 | def test_bad_method(self, crossbar): 31 | wamp = AutobahnSync() 32 | 33 | @wamp.on_challenge 34 | def on_challenge(challenge): 35 | raise Exception("Invalid authmethod %s" % challenge.method) 36 | 37 | with pytest.raises(AbortError) as exc: 38 | wamp.run(realm=u'realm1', url=u'ws://localhost:8080/ws_auth', authid='ticket_user_1', authmethods=[u'ticket']) 39 | assert str(exc.value.args[0]) == 'Invalid authmethod ticket' 40 | 41 | def test_good_auth(self, crossbar): 42 | wamp = AutobahnSync() 43 | 44 | @wamp.on_challenge 45 | def on_challenge(challenge): 46 | assert challenge.method == 'ticket' 47 | return 'ticket_secret' 48 | 49 | wamp.run(realm=u'realm1', url=u'ws://localhost:8080/ws_auth', authid='ticket_user_1', authmethods=[u'ticket']) 50 | # Make sure we are connected 51 | wamp.session.subscribe(lambda: None, u'test.challenge.event') 52 | -------------------------------------------------------------------------------- /tests/test_flask.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from flask import Flask 4 | from time import sleep 5 | from autobahn_sync.extensions.flask import FlaskAutobahnSync 6 | from autobahn_sync import NotRunningError, AbortError, ConnectionRefusedError 7 | from autobahn.wamp import PublishOptions 8 | 9 | from fixtures import crossbar, wamp 10 | 11 | 12 | class TestFlask(object): 13 | 14 | 15 | def setup(self): 16 | self.rpc_called = False 17 | self.sub_called = False 18 | 19 | def test_flask(self, crossbar): 20 | app = Flask(__name__) 21 | wamp = FlaskAutobahnSync() 22 | 23 | publish_opt = PublishOptions(exclude_me=False) 24 | 25 | @wamp.register('flask.flask.rpc') 26 | def rpc(): 27 | self.rpc_called = True 28 | 29 | @wamp.subscribe('flask.flask.event') 30 | def sub(): 31 | self.sub_called = True 32 | 33 | with pytest.raises(NotRunningError): 34 | wamp.session.call('flask.flask.rpc') 35 | 36 | with pytest.raises(NotRunningError): 37 | wamp.session.publish('flask.flask.event', options=publish_opt) 38 | 39 | assert not self.rpc_called 40 | assert not self.sub_called 41 | 42 | wamp.init_app(app) 43 | 44 | wamp.session.call('flask.flask.rpc') 45 | wamp.session.publish('flask.flask.event', options=publish_opt) 46 | sleep(0.1) # Dirty way to wait for on_event to be called... 47 | 48 | assert self.rpc_called 49 | assert self.sub_called 50 | 51 | def test_bad_config(self): 52 | app = Flask(__name__) 53 | with pytest.raises(AbortError): 54 | wamp = FlaskAutobahnSync(app, realm=u'bad_realm') 55 | with pytest.raises(ConnectionRefusedError): 56 | wamp = FlaskAutobahnSync(app, router=u'ws://localhost:9999/missing') 57 | -------------------------------------------------------------------------------- /tests/test_pubsub.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from time import sleep 3 | 4 | from autobahn_sync import AutobahnSync, ConnectionRefusedError, AbortError 5 | from autobahn.wamp import PublishOptions, ApplicationError 6 | 7 | from fixtures import crossbar, wamp, wamp2 8 | 9 | 10 | class TestBadRouter(object): 11 | 12 | def test_no_subscribe(self, crossbar): 13 | wamp = AutobahnSync() 14 | wamp.run(realm=u'realm_limited') 15 | with pytest.raises(ApplicationError) as exc: 16 | wamp.session.subscribe(lambda: None, 'pubsub.no_subscribe.event') 17 | assert str(exc.value.args[0]) == "session is not authorized to subscribe to topic 'pubsub.no_subscribe.event'" 18 | 19 | def test_no_publish(self, crossbar): 20 | wamp = AutobahnSync() 21 | wamp.run(realm=u'realm_limited') 22 | wamp.session.subscribe(lambda: None, 'pubsub.no_publish.event') 23 | publish_opt = PublishOptions(acknowledge=True) 24 | with pytest.raises(ApplicationError) as exc: 25 | res = wamp.session.publish(u'pubsub.no_publish.event', None, options=publish_opt) 26 | assert str(exc.value.args[0]) == u"session not authorized to publish to topic 'pubsub.no_publish.event'" 27 | 28 | def test_use_session(self, wamp, wamp2): 29 | events = [] 30 | 31 | def on_event(*args, **kwargs): 32 | events.append((args, kwargs)) 33 | 34 | sub = wamp2.session.subscribe(on_event, 'pubsub.use_session.event') 35 | wamp.session.publish('pubsub.use_session.event', '1') 36 | wamp.session.publish('pubsub.use_session.event', '2') 37 | wamp.session.publish('pubsub.use_session.event', opt=True) 38 | sleep(0.1) # Dirty way to wait for on_event to be called... 39 | assert events == [(('1',), {}), (('2',), {}), ((), {'opt': True})] 40 | 41 | def test_unsubscribe(self, wamp, wamp2): 42 | events = [] 43 | 44 | def on_event(*args, **kwargs): 45 | events.append((args, kwargs)) 46 | 47 | reg = wamp2.session.subscribe(on_event, 'pubsub.unsubscribe.event') 48 | wamp.session.publish('pubsub.unsubscribe.event', '1') 49 | # reg.unsubscribe() # Cannot use the default API so far... 50 | sleep(0.1) # Dirty way to wait for on_event to be called... 51 | wamp.session.unsubscribe(reg) 52 | wamp.session.publish('pubsub.unsubscribe.event', '2') 53 | sleep(0.1) # Dirty way to wait for on_event to be called... 54 | # Cannot unsubscribe 2 times 55 | with pytest.raises(Exception) as exc: 56 | wamp.session.unsubscribe(reg) 57 | assert str(exc.value.args[0]) == 'subscription no longer active' 58 | assert events == [(('1',), {})] 59 | 60 | def test_single_wamp_use_session(self, wamp): 61 | events = [] 62 | 63 | def on_event(*args, **kwargs): 64 | events.append((args, kwargs)) 65 | 66 | publish_opt = PublishOptions(exclude_me=False) 67 | sub = wamp.session.subscribe(on_event, 'pubsub.single_wamp_use_session.event') 68 | wamp.session.publish('pubsub.single_wamp_use_session.event', '1', options=publish_opt) 69 | wamp.session.publish('pubsub.single_wamp_use_session.event', '2', options=publish_opt) 70 | wamp.session.publish('pubsub.single_wamp_use_session.event', opt=True, options=publish_opt) 71 | sleep(0.1) # Dirty way to wait for on_event to be called... 72 | assert events == [(('1',), {}), (('2',), {}), ((), {'opt': True})] 73 | 74 | def test_use_decorator(self, wamp): 75 | events = [] 76 | 77 | @wamp.subscribe(u'pubsub.use_decorator.event') 78 | def on_event(*args, **kwargs): 79 | events.append((args, kwargs)) 80 | 81 | publish_opt = PublishOptions(exclude_me=False) 82 | wamp.session.publish('pubsub.use_decorator.event', '1', options=publish_opt) 83 | wamp.session.publish('pubsub.use_decorator.event', '2', options=publish_opt) 84 | wamp.session.publish('pubsub.use_decorator.event', opt=True, options=publish_opt) 85 | sleep(0.1) # Dirty way to wait for on_event to be called... 86 | assert events == [(('1',), {}), (('2',), {}), ((), {'opt': True})] 87 | 88 | def test_decorate_before_run(self, crossbar): 89 | events = [] 90 | wamp = AutobahnSync() 91 | 92 | @wamp.subscribe(u'pubsub.decorate_before_run.event') 93 | def on_event(*args, **kwargs): 94 | events.append((args, kwargs)) 95 | 96 | wamp.run() 97 | publish_opt = PublishOptions(exclude_me=False) 98 | wamp.session.publish('pubsub.decorate_before_run.event', '1', options=publish_opt) 99 | wamp.session.publish('pubsub.decorate_before_run.event', '2', options=publish_opt) 100 | wamp.session.publish('pubsub.decorate_before_run.event', opt=True, options=publish_opt) 101 | sleep(0.1) # Dirty way to wait for on_event to be called... 102 | assert events == [(('1',), {}), (('2',), {}), ((), {'opt': True})] 103 | 104 | def test_on_exception(self, wamp): 105 | events = [] 106 | class MyException(Exception): 107 | pass 108 | 109 | @wamp.subscribe(u'pubsub.on_exception.event') 110 | def on_event(*args, **kwargs): 111 | events.append((args, kwargs)) 112 | raise MyException('Ooops !') 113 | 114 | publish_opt = PublishOptions(exclude_me=False, acknowledge=True) 115 | wamp.session.publish('pubsub.on_exception.event', '1', options=publish_opt) 116 | sleep(0.1) # Dirty way to wait for on_event to be called... 117 | assert events == [(('1',), {})] 118 | -------------------------------------------------------------------------------- /tests/test_rpc.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from autobahn_sync import AutobahnSync 4 | from autobahn.wamp.exception import ApplicationError 5 | 6 | from fixtures import crossbar, wamp, wamp2 7 | 8 | 9 | class CounterHelper(object): 10 | def __init__(self): 11 | self.counter = 0 12 | 13 | def __call__(self): 14 | self.counter += 1 15 | return self.counter 16 | 17 | 18 | class TestRPC(object): 19 | 20 | def test_no_register(self, crossbar): 21 | wamp = AutobahnSync() 22 | wamp.run(realm=u'realm_limited') 23 | with pytest.raises(ApplicationError) as exc: 24 | wamp.session.register(lambda: None, u'rpc.no_register.func') 25 | assert str(exc.value.args[0]) == u"session is not authorized to register procedure 'rpc.no_register.func'" 26 | 27 | def test_no_call(self, crossbar): 28 | wamp = AutobahnSync() 29 | wamp.run(realm=u'realm_limited') 30 | wamp.session.register(lambda: None, u'rpc.no_call.func') 31 | with pytest.raises(ApplicationError) as exc: 32 | wamp.session.call(u'rpc.no_call.func', None) 33 | assert str(exc.value.args[0]) == u"session is not authorized to call procedure 'rpc.no_call.func'" 34 | 35 | def test_history(self, wamp): 36 | # First create some stuff in the history 37 | wamp.session.publish('rpc.historized.event', 1) 38 | wamp.session.publish('rpc.historized.event', '2') 39 | wamp.session.publish('rpc.historized.event', args=True) 40 | 41 | # Arriving too late to get notified of the events... 42 | sub = wamp.session.subscribe(lambda: None, 'rpc.historized.event') 43 | # ...but history is here for this ! 44 | events = wamp.session.call('wamp.subscription.get_events', sub.id, 3) 45 | 46 | assert len(events) == 3 47 | assert not [e for e in events if e[u'topic'] != u'rpc.historized.event'] 48 | assert [e[u'kwargs'] for e in events] == [{'args': True}, None, None] 49 | assert [e[u'args'] for e in events] == [[], ['2'], [1]] 50 | 51 | def test_bad_history(self, wamp): 52 | # Cannot get history on this one, should raise exception then 53 | sub = wamp.session.subscribe(lambda: None, 'rpc.not_historized.event') 54 | with pytest.raises(ApplicationError) as exc: 55 | events = wamp.session.call('wamp.subscription.get_events', sub.id, 10) 56 | assert str(exc.value.error_message()) == u'wamp.error.history_unavailable: ' 57 | 58 | @pytest.mark.xfail(reason='sub.id seems still valid even after unsubscribe') 59 | def test_history_on_unsubscribed(self, wamp): 60 | # Cannot get history on this one, should raise exception then 61 | sub = wamp.session.subscribe(lambda: None, 'rpc.historized.event') 62 | wamp.session.unsubscribe(sub) 63 | with pytest.raises(ApplicationError) as exc: 64 | events = wamp.session.call('wamp.subscription.get_events', sub.id, 10) 65 | assert exc.value.error == u'wamp.error.no_such_subscription' 66 | 67 | def test_use_session(self, wamp, wamp2): 68 | rets = [] 69 | counter_func = CounterHelper() 70 | sub = wamp2.session.register(counter_func, 'rpc.use_session.func') 71 | rets.append(wamp.session.call('rpc.use_session.func')) 72 | rets.append(wamp.session.call('rpc.use_session.func')) 73 | rets.append(wamp.session.call('rpc.use_session.func')) 74 | assert rets == [1, 2, 3] 75 | 76 | def test_unregister(self, wamp, wamp2): 77 | reg = wamp2.session.register(lambda: None, 'rpc.unregister.func') 78 | wamp.session.call('rpc.unregister.func') 79 | # reg.unregister() # Cannot use the default API so far... 80 | wamp.session.unregister(reg) 81 | with pytest.raises(ApplicationError) as exc: 82 | wamp.session.call('rpc.unregister.func') 83 | assert str(exc.value.args[0]) == u'no callee registered for procedure ' 84 | # Cannot unregister 2 times 85 | with pytest.raises(Exception) as exc: 86 | wamp.session.unregister(reg) 87 | assert str(exc.value.args[0]) == 'registration no longer active' 88 | 89 | def test_single_wamp_use_session(self, wamp): 90 | rets = [] 91 | counter_func = CounterHelper() 92 | sub = wamp.session.register(counter_func, 'rpc.single_wamp_use_session.func') 93 | rets.append(wamp.session.call('rpc.single_wamp_use_session.func')) 94 | rets.append(wamp.session.call('rpc.single_wamp_use_session.func')) 95 | rets.append(wamp.session.call('rpc.single_wamp_use_session.func')) 96 | assert rets == [1, 2, 3] 97 | 98 | def test_use_decorator(self, wamp): 99 | rets = [] 100 | counter_func = CounterHelper() 101 | 102 | @wamp.register(u'rpc.use_decorator.func') 103 | def my_func(*args, **kwargs): 104 | return counter_func() 105 | 106 | rets.append(wamp.session.call('rpc.use_decorator.func')) 107 | rets.append(wamp.session.call('rpc.use_decorator.func')) 108 | rets.append(wamp.session.call('rpc.use_decorator.func')) 109 | assert rets == [1, 2, 3] 110 | 111 | def test_decorate_before_run(self, crossbar): 112 | wamp = AutobahnSync() 113 | rets = [] 114 | counter_func = CounterHelper() 115 | 116 | @wamp.register('rpc.decorate_before_run.func') 117 | def my_func(*args, **kwargs): 118 | return counter_func() 119 | 120 | wamp.run() 121 | rets.append(wamp.session.call('rpc.decorate_before_run.func')) 122 | rets.append(wamp.session.call('rpc.decorate_before_run.func')) 123 | rets.append(wamp.session.call('rpc.decorate_before_run.func')) 124 | assert rets == [1, 2, 3] 125 | 126 | def test_on_exception(self, wamp): 127 | class MyException(Exception): 128 | pass 129 | 130 | @wamp.register(u'rpc.on_exception.func') 131 | def my_func(*args, **kwargs): 132 | raise MyException('Ooops !') 133 | with pytest.raises(ApplicationError) as exc: 134 | wamp.session.call('rpc.on_exception.func') 135 | assert str(exc.value.args[0]) == 'Ooops !' 136 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist=py27,py34,pypy 3 | [testenv] 4 | deps= 5 | -rdev-requirements.txt 6 | pytest-cov 7 | commands= 8 | flake8 . 9 | py.test --cov={envsitepackagesdir}/autobahn_sync 10 | --------------------------------------------------------------------------------