├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.md ├── channels_jsonrpc ├── __init__.py └── jsonrpcconsumer.py ├── example ├── django_example │ ├── __init__.py │ ├── consumer.py │ ├── routing.py │ ├── settings.py │ ├── tests.py │ ├── urls.py │ └── wsgi.py └── manage.py ├── requirements.txt ├── setup.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | venv3/ 84 | ENV/ 85 | 86 | # Spyder project settings 87 | .spyderproject 88 | 89 | # Rope project settings 90 | .ropeproject 91 | 92 | #pycharm prohject settings 93 | .idea/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - '2.7' 4 | - '3.6' 5 | install: 6 | - pip install -r requirements.txt 7 | script: 8 | - tox 9 | env: 10 | - TOXENV= 11 | - TOXENV=coverage 12 | notifications: 13 | slack: 14 | secure: QVhlHVTMjtt5xwpF2936+gdB3iriawm4DyKd3kWMD8IPvXER+z7xSti41FQVIP99bkkfmHxvmBu/e7Sofvn7W/aK4LnZ31p1AkvnR7v0VGH6lZ8KbJFk5CJV5cR8pDRigPOB3D4i09MhiBtTdRBc/rNFiBWmJ0mwze6K8y5Bm5Mg/Ai26Fn/bMs9nxxoTDzcjgKMHelKwUatMaTCT2E1mKJftJ7KNXK5b0AoAd0T15nQ/z/0RaiRc6NKypr14kuNFJARVRR48FfSNsGKhLNy1dJKEAdPiOrD4I9VC6tbnOwhG6pc9zLD6sUlmIt6bdmJQTutN8WG06PSD5SiX78KZqsxpsfvkXsDulw+lwlI5Fb97G9MLNPVxTou2jyymm0p/DEJa9S+lDrzuuMEgipYRJxl7KtOf9kib+A4nYlQUgqXm9TSJsTpWo00+oF+EuKmKz0mqwbGXDWqZ0L0InXZ4hY0j6EYLpt/47BORBevNJjn1UDzWfDOAa0h55YLKuKiRcxuLofGeVoBES0gYdM2Q9dMWV/9qQPI5LiAFjvl+yU4rY1iue67KbPUxqaInUWaMofAgY0su2EYsevndQaWUrngkuI8HnxGNeTVMFJAzGDUm28FfbWUfHs6a7Muf8DkHRNtXbJDp6+yAwuFMYPxOTFgzhctNgRKx5JRjbvYmTU= 15 | email: 16 | recipients: 17 | - fab@mozaik.life 18 | on_success: change 19 | on_failure: always 20 | before_deploy: 21 | - export RELEASE_PKG_FILE=$(ls *.tar.gz) 22 | - echo "deploying $RELEASE_PKG_FILE to GitHub releases" 23 | - echo ls /home/travis/build/millerf/django-channels-jsonrpc 24 | deploy: 25 | - provider: pypi 26 | user: "__token__" 27 | password: pypi-AgEIcHlwaS5vcmcCJGExMDIxZjRhLTFhZDMtNDc4YS1iOWNmLWQwMzY0OTIwZjFjNwACSHsicGVybWlzc2lvbnMiOiB7InByb2plY3RzIjogWyJkamFuZ28tY2hhbm5lbHMtanNvbnJwYyJdfSwgInZlcnNpb24iOiAxfQAABiBZg48cIBQt7HckwM4G3q-462xphsLbm7IZvjqMS4jvQw 28 | on: 29 | tags: true 30 | distributions: sdist bdist_wheel 31 | repo: millerf/django-channels-jsonrpc 32 | addons: 33 | code_climate: 34 | repo_token: 485981332850be046c9a8c0beda9a864ae65e7649d2f8dc2ef447e39064df2a2 35 | 36 | # - provider: releases 37 | # api_key: 38 | # secure: eWNcRlFoImCTciWy4YNCCtNbn2Klu9Q8kOAu5Stusobve+LdkuWrXwxDamtCMAoKfVR76ckQHdWIoTKxQYyIrZzHGneOCZcnnvdPN8olOW9ll3VHS6l1tuPW/LXmJ/Syqwm+4vFItspW5EJvaawhK3ETBJBA4A3TdbxpAWTAmAXG/aSSKodYjiAv7ZOKEP9fXfKIGu0rDd0ZGf37cqZ4OIuMM3GYE+AGL6mINwbhgdtag5v1LhpmghqW0kHWhDPlYRqZWe971cGt0HXxGpWqALthTmQTPfz5qXQ6eqZRsqGTv8xRWZo7kqqW53dKs8uVXltAErWNIkjzSkys8PchiwLogHg8Y6TdHo77xeMCXCDKz8qroFmRO4aj12/MVJMsCo2vNiVTbHzdzpy0NHp4p4ZyGFcjBYnmH7K1THWcWkRUILSjsVUhgG+jO2NdzAzADUai8HdLS1Jexmiq1YUMX8N53oseoSjBz1iuKJFWEDL+i/nERE813nyjtrs4jYYQ8BORWkJ/dTiZf8h28F83kgIkA8Rfl8U8hXqYgXW/K7fE2z5xlVLUthFbxOCGYXbRwZi+kUD8oo54mD0MO1moviLQQBdE6k+QUNyA7R4cMJC4YnAr3ZIzxNWzzPHCkFD7dfJOv/fxQ8nfRSVvKk+xTwA7SZ9tjff3m0KalLCS7xs= 39 | # file_glob: true 40 | # file: "${RELEASE_PKG_FILE}" 41 | # on: 42 | # repo: millerf/django-channels-jsonrpc 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 MILLER/F 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md 3 | recursive-include docs * 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django-channels-jsonrpc 2 | 3 | -------------- 4 | 5 | 6 | 7 | For channels 2, see [here](https://github.com/millerf/django-channels2-jsonrpc) 8 | 9 | 10 | 11 | -------------- 12 | 13 | 14 | [![PyPI version](https://badge.fury.io/py/django-channels-jsonrpc.svg)](https://badge.fury.io/py/django-channels-jsonrpc) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/04d12270939d47689756edda41e9f69f)](https://www.codacy.com/app/MosaicVenture/django-channels-jsonrpc?utm_source=github.com&utm_medium=referral&utm_content=millerf/django-channels-jsonrpc&utm_campaign=badger) [![Build Status](https://travis-ci.org/millerf/django-channels-jsonrpc.svg?branch=master)](https://travis-ci.org/millerf/django-channels-jsonrpc) [![Coverage Status](https://coveralls.io/repos/github/millerf/django-channels-jsonrpc/badge.svg)](https://coveralls.io/github/millerf/django-channels-jsonrpc) [![Code Climate](https://codeclimate.com/github/millerf/django-channels-jsonrpc/badges/gpa.svg)](https://codeclimate.com/github/millerf/django-channels-jsonrpc) 15 | 16 | The Django-channels-jsonrpc is aimed to enable [JSON-RPC](http://json-rpc.org/) functionnality on top of the excellent django channels project and especially their Websockets functionality. 17 | It is aimed to be: 18 | - Fully integrated with Channels 19 | - Fully implement JSON-RPC 1 and 2 protocol 20 | - Support both WebSocket and HTTP transports 21 | - Easy integration 22 | 23 | ## Tech 24 | 25 | 26 | The only Django-channels-jsonrpc dependency is the [Django channels project](https://github.com/django/channels) 27 | 28 | ## Installation 29 | 30 | 31 | Download and extract the [latest pre-built release](https://github.com/joemccann/dillinger/releases). 32 | 33 | Install the dependencies and devDependencies and start the server. 34 | 35 | ```sh 36 | $ pip install django-channels-jsonrpc 37 | ``` 38 | 39 | 40 | ## Use 41 | 42 | 43 | See complete example [here](https://github.com/millerf/django-channels-jsonrpc/blob/master/example/django_example/), and in particular [consumer.py](https://github.com/millerf/django-channels-jsonrpc/blob/master/example/django_example/) 44 | 45 | It is intended to be used as a Websocket consumer. See [documentation](http://channels.readthedocs.io/en/stable/generics.html#websockets) except... simplier... 46 | 47 | Import JsonRpcConsumer class and create the consumer 48 | 49 | ```python 50 | from channels_jsonrpc import JsonRpcConsumer 51 | 52 | class MyJsonRpcConsumer(JsonRpcConsumer): 53 | 54 | def connect(self, message, **kwargs): 55 | """ 56 | Perform things on WebSocket connection start 57 | """ 58 | self.message.reply_channel.send({"accept": True}) 59 | 60 | print("connect") 61 | # Do stuff if needed 62 | 63 | def disconnect(self, message, **kwargs): 64 | """ 65 | Perform things on WebSocket connection close 66 | """ 67 | print("disconnect") 68 | # Do stuff if needed 69 | 70 | ``` 71 | JsonRpcConsumer derives from Channels WebSocketConsumer, you can read about all it's features here: 72 | https://channels.readthedocs.io/en/stable/generics.html#websockets 73 | 74 | Then the last step is to create the RPC methos hooks. IT is done with the decorator: 75 | ```python 76 | @MyJsonRpcConsumer.rpc_method() 77 | ```` 78 | 79 | 80 | Like this: 81 | 82 | ```python 83 | @MyJsonRpcConsumer.rpc_method() 84 | def ping(): 85 | return "pong" 86 | ``` 87 | 88 | 89 | **MyJsonRpcConsumer.rpc_method()** accept a *string* as a parameter to 'rename' the function 90 | ```python 91 | @MyJsonRpcConsumer.rpc_method("mymodule.rpc.ping") 92 | def ping(): 93 | return "pong" 94 | ``` 95 | 96 | Will now be callable with "method":"mymodule.rpc.ping" in the rpc call: 97 | ```javascript 98 | {"id":1, "jsonrpc":"2.0","method":"mymodule.rpc.ping","params":{}} 99 | ``` 100 | 101 | RPC methods can obviously accept parameters. They also return "results" or "errors": 102 | ```python 103 | @MyJsonRpcConsumer.rpc_method("mymodule.rpc.ping") 104 | def ping(fake_an_error): 105 | if fake_an_error: 106 | # Will return an error to the client 107 | # --> {"id":1, "jsonrpc":"2.0","method":"mymodule.rpc.ping","params":{}} 108 | # <-- {"id": 1, "jsonrpc": "2.0", "error": {"message": "fake_error", "code": -32000, "data": ["fake_error"]}} 109 | raise Exception("fake_error") 110 | else: 111 | # Will return a result to the client 112 | # --> {"id":1, "jsonrpc":"2.0","method":"mymodule.rpc.ping","params":{}} 113 | # <-- {"id": 1, "jsonrpc": "2.0", "result": "pong"} 114 | return "pong" 115 | ``` 116 | 117 | ## [Sessions and other parameters from Message object](#message-object) 118 | The original channel message - that can contain sessions (if activated with [http_user](https://channels.readthedocs.io/en/stable/generics.html#websockets)) and other important info can be easily accessed by retrieving the `**kwargs` and get a parameter named *original_message* 119 | 120 | ```python 121 | MyJsonRpcConsumerTest.rpc_method() 122 | def json_rpc_method(param1, **kwargs): 123 | original_message = kwargs["orginal_message"] 124 | ##do something with original_message 125 | ``` 126 | 127 | Example: 128 | 129 | ```python 130 | class MyJsonRpcConsumerTest(JsonRpcConsumer): 131 | # Set to True to automatically port users from HTTP cookies 132 | # (you don't need channel_session_user, this implies it) 133 | # https://channels.readthedocs.io/en/stable/generics.html#websockets 134 | http_user = True 135 | 136 | .... 137 | 138 | @MyJsonRpcConsumerTest.rpc_method() 139 | def ping(**kwargs): 140 | original_message = kwargs["orginal_message"] 141 | original_message.channel_session["test"] = True 142 | return "pong" 143 | 144 | 145 | ``` 146 | 147 | ## Notifications 148 | ### Inbound notifications 149 | Those are the one sent from the client to the server. 150 | They are dealt with the same way RPC methods are, except that instead of using `rpc_method()`, you can use `rpc_notification()` 151 | Thos `rpc_notifications` can also retrieve the [`original_message`](#message-object) object 152 | ``` 153 | # Will be triggered when receiving this 154 | # --> {"jsonrpc":"2.0","method":"notification.alt_name","params":["val_param1", "val_param2"]} 155 | @MyJsonRpcWebsocketConsumerTest.rpc_notification("notification.alt_name") 156 | def notification1(param1, param2, **kwargs): 157 | original_message = kwargs["orginal_message"] 158 | # Do something with notification 159 | # ... 160 | # Notification shouldn't return anything. 161 | return 162 | ``` 163 | 164 | ### Outbound notifications 165 | The server might want to send notifications to one or more of its clients. For that `JsonRpcWebsocketConsumer` provides 2 static methods: 166 | - **JsonRpcWebsocketConsumer.notify_group(*group_name*, *method*, *params*)** 167 | 168 | Using [channels'groups](https://channels.readthedocs.io/en/stable/concepts.html#groups) you can notify a whole group using this method 169 | ``` 170 | @MyJsonRpcWebsocketConsumerTest.rpc_method() 171 | def send_to_group(group_name): 172 | MyJsonRpcWebsocketConsumerTest.notify_group(group_name, "notification.notif", {"payload": 1234}) 173 | return True 174 | ``` 175 | Calling the RPC-method will send this notification to all the group *group_name* 176 | 177 | 178 | - **JsonRpcWebsocketConsumer.notify_channel(*reply_channel*, *method*, *params*)** 179 | 180 | This will notify only *one* channel/client. 181 | 182 | ``` 183 | @MyJsonRpcWebsocketConsumerTest.rpc_method() 184 | def send_to_reply_channel(**kwargs): 185 | original_message = kwarg["original_message"] 186 | MyJsonRpcWebsocketConsumerTest.notify_channel(original_message.reply_channel, 187 | "notification.ownnotif", 188 | {"payload": 12}) 189 | return True 190 | 191 | ``` 192 | 193 | The `reply_channel` can be found in the[`original_message`](#message-object) object. 194 | 195 | ### Transport-specific rpc-method/notifications 196 | If you want to restrict rpc methods or notifications access to a specific transport method (http or websocket) 197 | The two decorator `rpc_method()` and `rpc_notification()` accept parameters to restric their use. `websocket` (default: True) and `http` (default: True) 198 | 199 | You can use them like this: 200 | ``` 201 | @MyJsonRpcWebsocketConsumerTest.rpc_notification("notification.alt_name", websocket=True, http=False) 202 | def notification1(param1, param2, **kwargs): 203 | original_message = kwargs["orginal_message"] 204 | # This notification will only be used when using websocket transport 205 | return 206 | ``` 207 | 208 | 209 | 210 | ## Custom JSON encoder class 211 | 212 | ```python 213 | from django.core.serializers.json import DjangoJSONEncoder 214 | 215 | 216 | class DjangoJsonRpcConsumer(JsonRpcConsumer): 217 | json_encoder_class = DjangoJSONEncoder 218 | ``` 219 | 220 | ## Testing 221 | 222 | 223 | The JsonRpcConsumer class can be tested the same way Channels Consumers are tested. 224 | See [here](http://channels.readthedocs.io/en/stable/testing.html) 225 | 226 | You just need to remember to set your JsonRpcConsumer class to TEST_MODE in the test: 227 | 228 | ```python 229 | from channels.tests import ChannelTestCase, HttpClient 230 | from .consumer import MyJsonRpcConsumer 231 | 232 | MyJsonRpcConsumer.TEST_MODE = True 233 | 234 | 235 | 236 | class TestsJsonConsumer(ChannelTestCase): 237 | def assertResult(self, method, params, result, error=False): 238 | client = HttpClient() 239 | client.send_and_consume('websocket.receive', text=request(method, params)) 240 | key = "result" if not error else "error" 241 | message = client.receive() 242 | if message is None or key not in message: 243 | raise KeyError("'%s' key not in message: %s" % (key, message)) 244 | 245 | self.assertEquals(message[key], result) 246 | 247 | def assertError(self, method, params, result): 248 | self.assertResult(method, params, result, True) 249 | 250 | def test_assert_result(self): 251 | 252 | self.assertResult("ping", {}, "pong") 253 | ``` 254 | 255 | ## License 256 | 257 | 258 | MIT 259 | 260 | *Have fun with Websockets*! 261 | 262 | **Free Software, Hell Yeah!** 263 | 264 | [//]: # (These are reference links used in the body of this note and get stripped out when the markdown processor does its job. There is no need to format nicely because it shouldn't be seen. Thanks SO - http://stackoverflow.com/questions/4823468/store-comments-in-markdown-syntax) 265 | 266 | -------------------------------------------------------------------------------- /channels_jsonrpc/__init__.py: -------------------------------------------------------------------------------- 1 | from .jsonrpcconsumer import JsonRpcConsumer, JsonRpcConsumerTest, JsonRpcException 2 | -------------------------------------------------------------------------------- /channels_jsonrpc/jsonrpcconsumer.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import sys 4 | 5 | if sys.version_info < (3, 5): 6 | from inspect import getargspec as getfullargspec 7 | 8 | keywords_args = "keywords" 9 | else: 10 | from inspect import getfullargspec 11 | 12 | keywords_args = "varkw" 13 | 14 | from channels.generic.websockets import WebsocketConsumer 15 | from django.http import HttpResponse 16 | from django.conf import settings 17 | from channels.handler import AsgiHandler, AsgiRequest 18 | from six import string_types 19 | from corsheaders.middleware import CorsMiddleware 20 | 21 | # Get an instance of a logger 22 | logger = logging.getLogger(__name__) 23 | 24 | 25 | class JsonRpcException(Exception): 26 | """ 27 | >>> exc = JsonRpcException(1, JsonRpcConsumer.INVALID_REQUEST) 28 | >>> str(exc) 29 | '{"jsonrpc": "2.0", "id": 1, "error": {"message": "Invalid Request", "code": -32600}}' 30 | 31 | """ 32 | 33 | def __init__(self, rpc_id, code, data=None): 34 | self.rpc_id = rpc_id 35 | self.code = code 36 | self.data = data 37 | 38 | @property 39 | def message(self): 40 | return JsonRpcConsumer.errors[self.code] 41 | 42 | def as_dict(self): 43 | return JsonRpcConsumer.error(self.rpc_id, self.code, self.message, self.data) 44 | 45 | def __str__(self): 46 | return json.dumps(self.as_dict()) 47 | 48 | 49 | class MethodNotSupported(Exception): 50 | pass 51 | 52 | 53 | class JsonRpcConsumer(WebsocketConsumer): 54 | """ 55 | Variant of WebsocketConsumer that automatically JSON-encodes and decodes 56 | messages as they come in and go out. Expects everything to be text; will 57 | error on binary data. 58 | 59 | http://groups.google.com/group/json-rpc/web/json-rpc-2-0 60 | errors: 61 | code message meaning 62 | -32700 Parse error Invalid JSON was received by the server. 63 | An error occurred on the server while parsing the JSON text. 64 | -32600 Invalid Request The JSON sent is not a valid Request object. 65 | -32601 Method not found The method does not exist / is not available. 66 | -32602 Invalid params Invalid method parameter(s). 67 | -32603 Internal error Internal JSON-RPC error. 68 | -32099 to -32000 69 | Server error Reserved for implementation-defined server-errors. (@TODO) 70 | 71 | """ 72 | # Add http.request alogn with default websocket events 73 | method_mapping = { 74 | "websocket.connect": "raw_connect", 75 | "websocket.receive": "raw_receive", 76 | "websocket.disconnect": "raw_disconnect", 77 | "http.request": "http_handler" 78 | } 79 | 80 | PARSE_ERROR = -32700 81 | INVALID_REQUEST = -32600 82 | METHOD_NOT_FOUND = -32601 83 | INVALID_PARAMS = -32602 84 | INTERNAL_ERROR = -32603 85 | GENERIC_APPLICATION_ERROR = -32000 86 | 87 | errors = dict() 88 | errors[PARSE_ERROR] = "Parse Error" 89 | errors[INVALID_REQUEST] = "Invalid Request" 90 | errors[METHOD_NOT_FOUND] = "Method Not Found" 91 | errors[INVALID_PARAMS] = "Invalid Params" 92 | errors[INTERNAL_ERROR] = "Internal Error" 93 | errors[GENERIC_APPLICATION_ERROR] = "Application Error" 94 | 95 | _http_codes = { 96 | PARSE_ERROR: 500, 97 | INVALID_REQUEST: 400, 98 | METHOD_NOT_FOUND: 404, 99 | INVALID_PARAMS: 500, 100 | INTERNAL_ERROR: 500, 101 | GENERIC_APPLICATION_ERROR: 500 102 | } 103 | 104 | json_encoder_class = None 105 | 106 | available_rpc_methods = dict() 107 | available_rpc_notifications = dict() 108 | 109 | @classmethod 110 | def rpc_method(cls, rpc_name=None, websocket=True, http=True): 111 | """ 112 | Decorator to list RPC methodds available. An optional name and protocol rectrictions can be added 113 | :param rpc_name: RPC name for the function 114 | :param bool websocket: if websocket transport can use this function 115 | :param bool http:if http transport can use this function 116 | :return: decorated function 117 | """ 118 | 119 | def wrap(f): 120 | name = rpc_name if rpc_name is not None else f.__name__ 121 | cid = id(cls) 122 | if cid not in cls.available_rpc_methods: 123 | cls.available_rpc_methods[cid] = dict() 124 | f.options = dict(websocket=websocket, http=http) 125 | cls.available_rpc_methods[cid][name] = f 126 | 127 | return f 128 | 129 | return wrap 130 | 131 | @classmethod 132 | def get_rpc_methods(cls): 133 | """ 134 | Returns the RPC methods available for this consumer 135 | :return: list 136 | """ 137 | if id(cls) not in cls.available_rpc_methods: 138 | return [] 139 | return list(cls.available_rpc_methods[id(cls)].keys()) 140 | 141 | @classmethod 142 | def rpc_notification(cls, rpc_name=None, websocket=True, http=True): 143 | """ 144 | Decorator to list RPC notifications available. An optional name can be added 145 | :param rpc_name: RPC name for the function 146 | :param bool websocket: if websocket transport can use this function 147 | :param bool http:if http transport can use this function 148 | :return: decorated function 149 | """ 150 | 151 | def wrap(f): 152 | name = rpc_name if rpc_name is not None else f.__name__ 153 | cid = id(cls) 154 | if cid not in cls.available_rpc_notifications: 155 | cls.available_rpc_notifications[cid] = dict() 156 | f.options = dict(websocket=websocket, http=http) 157 | cls.available_rpc_notifications[cid][name] = f 158 | return f 159 | 160 | return wrap 161 | 162 | @classmethod 163 | def get_rpc_notifications(cls): 164 | """ 165 | Returns the RPC methods available for this consumer 166 | :return: list 167 | """ 168 | if id(cls) not in cls.available_rpc_notifications: 169 | return [] 170 | return list(cls.available_rpc_notifications[id(cls)].keys()) 171 | 172 | @staticmethod 173 | def json_rpc_frame(_id=None, result=None, params=None, method=None, error=None): 174 | frame = {'jsonrpc': '2.0'} 175 | if _id is not None: 176 | frame["id"] = _id 177 | if method: 178 | frame["method"] = method 179 | frame["params"] = params 180 | elif result is not None: 181 | frame["result"] = result 182 | elif error is not None: 183 | frame["error"] = error 184 | 185 | return frame 186 | 187 | @staticmethod 188 | def error(_id, code, message, data=None): 189 | """ 190 | Error-type answer generator 191 | :param _id: int 192 | :param code: code of the error 193 | :param message: message for the error 194 | :param data: (optional) error data 195 | :return: object 196 | """ 197 | error = {'code': code, 'message': message} 198 | if data is not None: 199 | error["data"] = data 200 | 201 | return JsonRpcConsumer.json_rpc_frame(error=error, _id=_id) 202 | 203 | def http_handler(self, message): 204 | """ 205 | Called on HTTP request 206 | :param message: message received 207 | :return: 208 | """ 209 | # Get Django HttpRequest object from ASGI Message 210 | request = AsgiRequest(message) 211 | 212 | # CORS 213 | response = CorsMiddleware().process_request(request) 214 | if not isinstance(response, HttpResponse): 215 | 216 | # Try to process content 217 | try: 218 | if request.method != 'POST': 219 | raise MethodNotSupported('Only POST method is supported') 220 | content = request.body.decode('utf-8') 221 | except (UnicodeDecodeError, MethodNotSupported): 222 | content = '' 223 | result, is_notification = self.__handle(content, message) 224 | 225 | # Set response status code 226 | # http://www.jsonrpc.org/historical/json-rpc-over-http.html#response-codes 227 | if not is_notification: 228 | # call response 229 | status_code = 200 230 | if 'error' in result: 231 | status_code = self._http_codes[result['error']['code']] 232 | else: 233 | # notification response 234 | status_code = 204 235 | if result and 'error' in result: 236 | status_code = self._http_codes[result['error']['code']] 237 | result = '' 238 | 239 | response = HttpResponse(self.__class__._encode(result), content_type='application/json-rpc', 240 | status=status_code) 241 | 242 | # CORS 243 | response = CorsMiddleware().process_response(request, response) 244 | 245 | # Encode that response into message format (ASGI) 246 | for chunk in AsgiHandler.encode_response(response): 247 | message.reply_channel.send(chunk) 248 | 249 | def raw_receive(self, message, **kwargs): 250 | """ 251 | Called when receiving a message. 252 | :param message: message received 253 | :param kwargs: 254 | :return: 255 | """ 256 | content = '' if "text" not in message else message["text"] 257 | result, is_notification = self.__handle(content, message) 258 | 259 | # Send responce back only if it is a call, not notification 260 | if not is_notification: 261 | self.send(text=self.__class__._encode(result)) 262 | 263 | def __handle(self, content, message): 264 | """ 265 | Handle 266 | :param content: 267 | :param message: 268 | :return: 269 | """ 270 | result = None 271 | is_notification = False 272 | if content != '': 273 | try: 274 | data = json.loads(content) 275 | except ValueError: 276 | # json could not decoded 277 | result = self.error(None, self.PARSE_ERROR, self.errors[self.PARSE_ERROR]) 278 | else: 279 | if isinstance(data, dict): 280 | 281 | try: 282 | if data.get('method') is not None and data.get('id') is None: 283 | is_notification = True 284 | result = self.__process(data, message, is_notification) 285 | except JsonRpcException as e: 286 | result = e.as_dict() 287 | except Exception as e: 288 | logger.debug('Application error', e) 289 | result = self.error(data.get('id'), 290 | self.GENERIC_APPLICATION_ERROR, 291 | str(e), 292 | e.args[0] if len(e.args) == 1 else e.args) 293 | elif isinstance(data, list): 294 | # TODO: implement batch calls 295 | if len([x for x in data if not isinstance(x, dict)]): 296 | result = self.error(None, self.INVALID_REQUEST, self.errors[self.INVALID_REQUEST]) 297 | 298 | else: 299 | result = self.error(None, self.INVALID_REQUEST, self.errors[self.INVALID_REQUEST]) 300 | 301 | return result, is_notification 302 | 303 | @classmethod 304 | def _encode(cls, data): 305 | """ 306 | Encode data object to JSON string 307 | :param data: 308 | :return: 309 | """ 310 | return json.dumps(data, cls=cls.json_encoder_class) 311 | 312 | @classmethod 313 | def notify_group(cls, group_name, method, params=None): 314 | """ 315 | Notify a group. Using JSON-RPC notificatons 316 | :param group_name: Group name 317 | :param method: JSON-RPC method 318 | :param params: parmas of the method 319 | :return: 320 | """ 321 | content = JsonRpcConsumer.json_rpc_frame(method=method, params=params) 322 | cls.group_send(group_name, cls._encode(content)) 323 | 324 | @classmethod 325 | def notify_channel(cls, reply_channel, method, params): 326 | """ 327 | Notify a group. Using JSON-RPC notificatons 328 | :param reply_channel: Reply channel 329 | :param method: JSON-RPC method 330 | :param params: parmas of the method 331 | :return: 332 | """ 333 | content = JsonRpcConsumer.json_rpc_frame(method=method, params=params) 334 | reply_channel.send({"text": cls._encode(content)}) 335 | 336 | @classmethod 337 | def __process(cls, data, original_msg, is_notification=False): 338 | """ 339 | Process the recived data 340 | :param dict data: 341 | :param channels.message.Message original_msg: 342 | :param bool is_notification: 343 | :return: dict 344 | """ 345 | 346 | if data.get('jsonrpc') != "2.0": 347 | raise JsonRpcException(data.get('id'), cls.INVALID_REQUEST) 348 | 349 | if 'method' not in data: 350 | raise JsonRpcException(data.get('id'), cls.INVALID_REQUEST) 351 | 352 | method_name = data['method'] 353 | if not isinstance(method_name, string_types): 354 | raise JsonRpcException(data.get('id'), cls.INVALID_REQUEST) 355 | 356 | if method_name.startswith('_'): 357 | raise JsonRpcException(data.get('id'), cls.METHOD_NOT_FOUND) 358 | 359 | try: 360 | if is_notification: 361 | method = cls.available_rpc_notifications[id(cls)][method_name] 362 | else: 363 | method = cls.available_rpc_methods[id(cls)][method_name] 364 | proto = original_msg.channel.name.split('.')[0] 365 | if not method.options[proto]: 366 | raise MethodNotSupported('Method not available through %s' % proto) 367 | except (KeyError, MethodNotSupported): 368 | raise JsonRpcException(data.get('id'), cls.METHOD_NOT_FOUND) 369 | params = data.get('params', []) 370 | 371 | if not isinstance(params, (list, dict)): 372 | raise JsonRpcException(data.get('id'), cls.INVALID_PARAMS) 373 | 374 | # log call in debug mode 375 | if settings.DEBUG: 376 | logger.debug('Executing %s(%s)' % (method_name, json.dumps(params))) 377 | 378 | result = JsonRpcConsumer.__get_result(method, params, original_msg) 379 | 380 | # check and pack result 381 | if not is_notification: 382 | # log call in debug mode 383 | if settings.DEBUG: 384 | logger.debug('Execution result: %s' % cls._encode(result)) 385 | 386 | result = JsonRpcConsumer.json_rpc_frame(result=result, _id=data.get('id')) 387 | elif result is not None: 388 | logger.warning("The notification method shouldn't return any result") 389 | logger.warning("method: %s, params: %s" % (method_name, params)) 390 | result = None 391 | 392 | return result 393 | 394 | @staticmethod 395 | def __get_result(method, params, original_msg): 396 | 397 | func_args = getattr(getfullargspec(method), keywords_args) 398 | 399 | if func_args and "kwargs" in func_args: 400 | if isinstance(params, list): 401 | result = method(*params, original_message=original_msg) 402 | else: 403 | result = method(original_message=original_msg, **params) 404 | else: 405 | if isinstance(params, list): 406 | result = method(*params) 407 | else: 408 | result = method(**params) 409 | 410 | return result 411 | 412 | 413 | class JsonRpcConsumerTest(JsonRpcConsumer): 414 | @classmethod 415 | def clean(cls): 416 | """ 417 | Clean the class method name for tests 418 | :return: None 419 | """ 420 | if id(cls) in cls.available_rpc_methods: 421 | del cls.available_rpc_methods[id(cls)] 422 | -------------------------------------------------------------------------------- /example/django_example/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millerf/django-channels-jsonrpc/b71bc9eb7571be11b08aa99d5008c6f3f6ae1866/example/django_example/__init__.py -------------------------------------------------------------------------------- /example/django_example/consumer.py: -------------------------------------------------------------------------------- 1 | from django.core.serializers.json import DjangoJSONEncoder 2 | 3 | from channels_jsonrpc import JsonRpcConsumerTest 4 | # import the logging library 5 | import logging 6 | 7 | # Get an instance of a logger 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | class MyJsonRpcWebsocketConsumerTest(JsonRpcConsumerTest): 12 | 13 | # Set to True if you want them, else leave out 14 | strict_ordering = False 15 | slight_ordering = False 16 | 17 | # Set to True to automatically port users from HTTP cookies 18 | # (you don't need channel_session_user, this implies it) 19 | # https://channels.readthedocs.io/en/stable/generics.html#websockets 20 | http_user = True 21 | 22 | def connection_groups(self, **kwargs): 23 | """ 24 | Called to return the list of groups to automatically add/remove 25 | this connection to/from. 26 | """ 27 | return ["test"] 28 | 29 | def connect(self, message, **kwargs): 30 | """ 31 | Perform things on connection start 32 | """ 33 | self.message.reply_channel.send({"accept": True}) 34 | logger.info("connect") 35 | 36 | # Do stuff if needed 37 | 38 | def disconnect(self, message, **kwargs): 39 | """ 40 | Perform things on connection close 41 | """ 42 | logger.info("disconnect") 43 | 44 | # Do stuff if needed 45 | 46 | def process(cls, data, original_msg): 47 | """ 48 | Made to test thread-safe 49 | :param data: 50 | :param original_msg: 51 | :return: 52 | """ 53 | return cls.__process(data, original_msg) 54 | 55 | 56 | @MyJsonRpcWebsocketConsumerTest.rpc_method() 57 | def ping(fake_an_error, **kwargs): 58 | if fake_an_error: 59 | # Will return an error to the client 60 | # --> {"id":1, "jsonrpc":"2.0","method":"mymodule.rpc.ping","params":{}} 61 | # <-- {"id": 1, "jsonrpc": "2.0", "error": {"message": "fake_error", "code": -32000, "data": ["fake_error"]}} 62 | raise Exception(False) 63 | else: 64 | # Will return a result to the client 65 | # --> {"id":1, "jsonrpc":"2.0","method":"mymodule.rpc.ping","params":{}} 66 | # <-- {"id": 1, "jsonrpc": "2.0", "result": "pong"} 67 | return "pong" 68 | 69 | 70 | class DjangoJsonRpcWebsocketConsumerTest(JsonRpcConsumerTest): 71 | json_encoder_class = DjangoJSONEncoder 72 | -------------------------------------------------------------------------------- /example/django_example/routing.py: -------------------------------------------------------------------------------- 1 | from .consumer import MyJsonRpcWebsocketConsumerTest, DjangoJsonRpcWebsocketConsumerTest 2 | 3 | 4 | channel_routing = [ 5 | DjangoJsonRpcWebsocketConsumerTest.as_route(path=r"^/django/$"), 6 | MyJsonRpcWebsocketConsumerTest.as_route(path=r""), 7 | ] 8 | -------------------------------------------------------------------------------- /example/django_example/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for django_channels_jsonrpc project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.10.5. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.10/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.10/ref/settings/ 11 | """ 12 | 13 | import os 14 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 15 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 16 | 17 | 18 | # Quick-start development settings - unsuitable for production 19 | # See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ 20 | 21 | # SECURITY WARNING: keep the secret key used in production secret! 22 | SECRET_KEY = '19qsynss^p#tkqnkej3vp99%+uy9xx9h8l=^2sk59()xw20@fa' 23 | 24 | # SECURITY WARNING: don't run with debug turned on in production! 25 | DEBUG = True 26 | 27 | ALLOWED_HOSTS = [] 28 | 29 | 30 | # Application definition 31 | 32 | INSTALLED_APPS = [ 33 | 'django.contrib.auth', 34 | 'django.contrib.contenttypes', 35 | 'django.contrib.sessions', 36 | 'channels' 37 | ] 38 | 39 | MIDDLEWARE = [ 40 | 'django.middleware.security.SecurityMiddleware', 41 | 'django.contrib.sessions.middleware.SessionMiddleware', 42 | 'django.middleware.common.CommonMiddleware', 43 | 'django.middleware.csrf.CsrfViewMiddleware', 44 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 45 | 'django.contrib.messages.middleware.MessageMiddleware', 46 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 47 | ] 48 | 49 | ROOT_URLCONF = 'django_example.urls' 50 | 51 | TEMPLATES = [ 52 | { 53 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 54 | 'DIRS': [], 55 | 'APP_DIRS': True, 56 | 'OPTIONS': { 57 | 'context_processors': [ 58 | 'django.template.context_processors.debug', 59 | 'django.template.context_processors.request', 60 | 'django.contrib.auth.context_processors.auth', 61 | 'django.contrib.messages.context_processors.messages', 62 | ], 63 | }, 64 | }, 65 | ] 66 | 67 | WSGI_APPLICATION = 'django_example.wsgi.application' 68 | 69 | 70 | # Database 71 | # https://docs.djangoproject.com/en/1.10/ref/settings/#databases 72 | 73 | DATABASES = { 74 | 'default': { 75 | 'ENGINE': 'django.db.backends.sqlite3', 76 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 77 | } 78 | } 79 | 80 | 81 | # Password validation 82 | # https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators 83 | 84 | AUTH_PASSWORD_VALIDATORS = [ 85 | { 86 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 87 | }, 88 | { 89 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 90 | }, 91 | { 92 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 93 | }, 94 | { 95 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 96 | }, 97 | ] 98 | 99 | 100 | # Internationalization 101 | # https://docs.djangoproject.com/en/1.10/topics/i18n/ 102 | 103 | LANGUAGE_CODE = 'en-us' 104 | 105 | TIME_ZONE = 'UTC' 106 | 107 | USE_I18N = True 108 | 109 | USE_L10N = True 110 | 111 | USE_TZ = True 112 | 113 | 114 | # Static files (CSS, JavaScript, Images) 115 | # https://docs.djangoproject.com/en/1.10/howto/static-files/ 116 | 117 | STATIC_URL = '/static/' 118 | 119 | CHANNEL_LAYERS = { 120 | "default": { 121 | "BACKEND": "asgiref.inmemory.ChannelLayer", 122 | "ROUTING": "django_example.routing.channel_routing" 123 | } 124 | } -------------------------------------------------------------------------------- /example/django_example/tests.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from channels_jsonrpc import JsonRpcConsumerTest, JsonRpcException 3 | from channels.tests import ChannelTestCase, HttpClient 4 | from .consumer import MyJsonRpcWebsocketConsumerTest, DjangoJsonRpcWebsocketConsumerTest 5 | 6 | 7 | class TestMyJsonRpcConsumer(JsonRpcConsumerTest): 8 | pass 9 | 10 | 11 | class TestMyJsonRpcConsumer2(JsonRpcConsumerTest): 12 | pass 13 | 14 | 15 | class TestsJsonRPCWebsocketConsumer(ChannelTestCase): 16 | 17 | def test_connection(self): 18 | # Test connection 19 | client = HttpClient() 20 | client.send_and_consume(u'websocket.connect') 21 | self.assertEquals(client.receive(), None) 22 | 23 | def test_response_are_well_formatted(self): 24 | # Answer should always json-rpc2 25 | client = HttpClient() 26 | client.send_and_consume(u'websocket.receive', {'value': 'my_value'}) 27 | 28 | response = client.receive() 29 | self.assertEqual(response['error'], 30 | {u'code': JsonRpcConsumerTest.INVALID_REQUEST, 31 | u'message': JsonRpcConsumerTest.errors[JsonRpcConsumerTest.INVALID_REQUEST]}) 32 | self.assertEqual(response['jsonrpc'], '2.0') 33 | if 'id' in response: 34 | self.assertEqual(response['id'], None) 35 | 36 | def test_inadequate_request(self): 37 | 38 | client = HttpClient() 39 | 40 | client.send_and_consume(u'websocket.receive', text='{"value": "my_value"}') 41 | self.assertEqual(client.receive()['error'], 42 | {u'code': JsonRpcConsumerTest.INVALID_REQUEST, 43 | u'message': JsonRpcConsumerTest.errors[JsonRpcConsumerTest.INVALID_REQUEST]}) 44 | 45 | client.send_and_consume(u'websocket.receive') 46 | self.assertEqual(client.receive()['error'], {u'code': JsonRpcConsumerTest.INVALID_REQUEST, 47 | u'message': JsonRpcConsumerTest.errors[ 48 | JsonRpcConsumerTest.INVALID_REQUEST]}) 49 | 50 | client.send_and_consume(u'websocket.receive', text='["value", "my_value"]') 51 | self.assertEqual(client.receive()['error'], {u'code': JsonRpcConsumerTest.INVALID_REQUEST, 52 | u'message': JsonRpcConsumerTest.errors[ 53 | JsonRpcConsumerTest.INVALID_REQUEST]}) 54 | 55 | # missing "method" 56 | client.send_and_consume(u'websocket.receive', text='{"id":"2", "jsonrpc":"2.0", "params":{}}') 57 | self.assertEqual(client.receive()['error'], 58 | {u'code': JsonRpcConsumerTest.INVALID_REQUEST, 59 | u'message': JsonRpcConsumerTest.errors[JsonRpcConsumerTest.INVALID_REQUEST]}) 60 | 61 | # wrong method name 62 | client.send_and_consume(u'websocket.receive', text='{"id":"2", "jsonrpc":"2.0", "method":2, "params":{}}') 63 | self.assertEqual(client.receive()['error'], {u'code': JsonRpcConsumerTest.INVALID_REQUEST, 64 | u'message': JsonRpcConsumerTest.errors[ 65 | JsonRpcConsumerTest.INVALID_REQUEST]}) 66 | 67 | # wrong method name 68 | client.send_and_consume(u'websocket.receive', text='{"id":"2", "jsonrpc":"2.0", "method":"_test", "params":{}}') 69 | self.assertEqual(client.receive()['error'], {u'code': JsonRpcConsumerTest.METHOD_NOT_FOUND, 70 | u'message': JsonRpcConsumerTest.errors[ 71 | JsonRpcConsumerTest.METHOD_NOT_FOUND]}) 72 | 73 | client.send_and_consume(u'websocket.receive', text='{"value": "my_value"}') 74 | self.assertEqual(client.receive()['error'], {u'code': JsonRpcConsumerTest.INVALID_REQUEST, 75 | u'message': JsonRpcConsumerTest.errors[ 76 | JsonRpcConsumerTest.INVALID_REQUEST]}) 77 | 78 | client.send_and_consume(u'websocket.receive', text='sqwdw') 79 | self.assertEqual(client.receive()['error'], 80 | {u'code': JsonRpcConsumerTest.PARSE_ERROR, 81 | u'message': JsonRpcConsumerTest.errors[JsonRpcConsumerTest.PARSE_ERROR]}) 82 | 83 | client.send_and_consume(u'websocket.receive', text='{}') 84 | self.assertEqual(client.receive()['error'], 85 | {u'code': JsonRpcConsumerTest.INVALID_REQUEST, 86 | u'message': JsonRpcConsumerTest.errors[JsonRpcConsumerTest.INVALID_REQUEST]}) 87 | 88 | client.send_and_consume(u'websocket.receive', text=None) 89 | self.assertEqual(client.receive()['error'], 90 | {u'code': JsonRpcConsumerTest.INVALID_REQUEST, 91 | u'message': JsonRpcConsumerTest.errors[JsonRpcConsumerTest.INVALID_REQUEST]}) 92 | 93 | def test_unexisting_method(self): 94 | # unknown method 95 | client = HttpClient() 96 | 97 | client.send_and_consume(u'websocket.receive', 98 | text='{"id": 1, "jsonrpc": "2.0", "method": "unknown_method", "params": {}}') 99 | msg = client.receive() 100 | self.assertEqual(msg['error'], {u'code': JsonRpcConsumerTest.METHOD_NOT_FOUND, 101 | u'message': JsonRpcConsumerTest.errors[JsonRpcConsumerTest.METHOD_NOT_FOUND]}) 102 | 103 | def test_parsing_with_bad_request(self): 104 | # Test that parsing a bad request works 105 | 106 | client = HttpClient() 107 | 108 | client.send_and_consume(u'websocket.receive', text='{"id":"2", "method":"ping2", "params":{}}') 109 | self.assertEqual(client.receive()['error'], 110 | {u'code': JsonRpcConsumerTest.INVALID_REQUEST, 111 | u'message': JsonRpcConsumerTest.errors[JsonRpcConsumerTest.INVALID_REQUEST]}) 112 | 113 | def test_notification(self): 114 | # Test that parsing a bad request works 115 | 116 | client = HttpClient() 117 | 118 | client.send_and_consume(u'websocket.receive', text='{"jsonrpc":"2.0", "method":"a_notif", "params":{}}') 119 | self.assertEqual(client.receive(), None) 120 | 121 | def test_method(self): 122 | 123 | @MyJsonRpcWebsocketConsumerTest.rpc_method() 124 | def ping2(): 125 | return "pong2" 126 | 127 | client = HttpClient() 128 | client.send_and_consume(u'websocket.receive', text='{"id":1, "jsonrpc":"2.0", "method":"ping2", "params":{}}') 129 | 130 | msg = client.receive() 131 | 132 | self.assertEqual(msg['result'], "pong2") 133 | 134 | def test_parsing_with_good_request_wrong_params(self): 135 | @JsonRpcConsumerTest.rpc_method() 136 | def ping2(): 137 | return "pong2" 138 | 139 | # Test that parsing a ping request works 140 | client = HttpClient() 141 | 142 | client.send_and_consume(u'websocket.receive', 143 | text='{"id":1, "jsonrpc":"2.0", "method":"ping2", "params":["test"]}') 144 | msg = client.receive() 145 | self.assertIn(msg['error']['message'], 146 | [u'ping2() takes no arguments (1 given)', # python 2 147 | u'ping2() takes 0 positional arguments but 1 was given']) # python 3 148 | 149 | def test_parsing_with_good_request_ainvalid_paramas(self): 150 | @JsonRpcConsumerTest.rpc_method() 151 | def ping2(test): 152 | return "pong2" 153 | 154 | # Test that parsing a ping request works 155 | client = HttpClient() 156 | 157 | client.send_and_consume(u'websocket.receive', 158 | text='{"id":1, "jsonrpc":"2.0", "method":"ping2", "params":true}') 159 | msg = client.receive() 160 | self.assertEqual(msg['error'], {u'code': JsonRpcConsumerTest.INVALID_PARAMS, 161 | u'message': JsonRpcConsumerTest.errors[ 162 | JsonRpcConsumerTest.INVALID_PARAMS]}) 163 | 164 | def test_parsing_with_good_request(self): 165 | # Test that parsing a ping request works 166 | client = HttpClient() 167 | 168 | client.send_and_consume(u'websocket.receive', 169 | text='{"id":1, "jsonrpc":"2.0", "method":"ping", "params":[false]}') 170 | msg = client.receive() 171 | self.assertEquals(msg['result'], "pong") 172 | 173 | def test_id_on_good_request(self): 174 | # Test that parsing a ping request works 175 | client = HttpClient() 176 | 177 | client.send_and_consume(u'websocket.receive', 178 | text='{"id":52, "jsonrpc":"2.0", "method":"ping", "params":{}}') 179 | msg = client.receive() 180 | self.assertEqual(msg['id'], 52) 181 | 182 | def test_id_on_errored_request(self): 183 | # Test that parsing a ping request works 184 | client = HttpClient() 185 | 186 | client.send_and_consume(u'websocket.receive', 187 | text='{"id":52, "jsonrpc":"2.0", "method":"ping", "params":["test"]}') 188 | msg = client.receive() 189 | self.assertEqual(msg['id'], 52) 190 | 191 | def test_get_rpc_methods(self): 192 | 193 | @TestMyJsonRpcConsumer.rpc_method() 194 | def ping3(): 195 | return "pong3" 196 | 197 | @TestMyJsonRpcConsumer2.rpc_method() 198 | def ping4(): 199 | return "pong4" 200 | 201 | methods = TestMyJsonRpcConsumer.get_rpc_methods() 202 | self.assertEquals(methods, ['ping3']) 203 | self.assertEquals(TestMyJsonRpcConsumer2.get_rpc_methods(), ['ping4']) 204 | 205 | def test_get_rpc_methods_with_name(self): 206 | 207 | class TestMyJsonRpcConsumer(JsonRpcConsumerTest): 208 | pass 209 | 210 | @TestMyJsonRpcConsumer.rpc_method('test.ping.rpc') 211 | def ping5(): 212 | return "pong5" 213 | 214 | self.assertEquals(TestMyJsonRpcConsumer.get_rpc_methods(), ['test.ping.rpc']) 215 | 216 | def test_error_on_rpc_call(self): 217 | @MyJsonRpcWebsocketConsumerTest.rpc_method() 218 | def ping_with_error(): 219 | raise Exception("pong_with_error") 220 | 221 | # Test that parsing a ping request works 222 | client = HttpClient() 223 | 224 | client.send_and_consume(u'websocket.receive', 225 | text='{"id":1, "jsonrpc":"2.0", "method":"ping_with_error", "params":{}}') 226 | msg = client.receive() 227 | self.assertEqual(msg['error']['message'], u'pong_with_error') 228 | 229 | def test_error_on_rpc_call_with_data(self): 230 | @MyJsonRpcWebsocketConsumerTest.rpc_method() 231 | def ping_with_error_data(): 232 | raise Exception("test_data", True) 233 | 234 | # Test that parsing a ping request works 235 | client = HttpClient() 236 | 237 | client.send_and_consume(u'websocket.receive', 238 | text='{"id":1, "jsonrpc":"2.0", "method":"ping_with_error_data", "params":{}}') 239 | msg = client.receive() 240 | self.assertEqual(msg['id'], 1) 241 | self.assertEqual(msg['error']['code'], JsonRpcConsumerTest.GENERIC_APPLICATION_ERROR) 242 | self.assertEqual(msg['error']['data'], ['test_data', True]) 243 | 244 | 245 | def test_JsonRpcWebsocketConsumerTest_clean(self): 246 | 247 | class TestNamesakeJsonRpcConsumer(JsonRpcConsumerTest): 248 | pass 249 | 250 | @TestNamesakeJsonRpcConsumer.rpc_method() 251 | def method_34(): 252 | pass 253 | 254 | self.assertIn("method_34", TestNamesakeJsonRpcConsumer.get_rpc_methods()) 255 | 256 | TestNamesakeJsonRpcConsumer.clean() 257 | 258 | self.assertEquals(TestNamesakeJsonRpcConsumer.get_rpc_methods(), []) 259 | 260 | def test_namesake_consumers(self): 261 | 262 | # Changed name to TestNamesakeJsonRpcConsumer2 to prevent overlapping with "previous" TestMyJsonRpcConsumer 263 | 264 | class Context1(): 265 | class TestNamesakeJsonRpcConsumer2(JsonRpcConsumerTest): 266 | pass 267 | 268 | class Context2(): 269 | class TestNamesakeJsonRpcConsumer2(JsonRpcConsumerTest): 270 | pass 271 | 272 | Context1.TestNamesakeJsonRpcConsumer2.clean() 273 | Context2.TestNamesakeJsonRpcConsumer2.clean() 274 | 275 | @Context1.TestNamesakeJsonRpcConsumer2.rpc_method() 276 | def method1(): 277 | pass 278 | 279 | @Context2.TestNamesakeJsonRpcConsumer2.rpc_method() 280 | def method2(): 281 | pass 282 | 283 | self.assertEquals(Context1.TestNamesakeJsonRpcConsumer2.get_rpc_methods(), ['method1']) 284 | self.assertEquals(Context2.TestNamesakeJsonRpcConsumer2.get_rpc_methods(), ['method2']) 285 | 286 | def test_no_rpc_methods(self): 287 | class TestNamesakeJsonRpcConsumer(JsonRpcConsumerTest): 288 | pass 289 | 290 | self.assertEquals(TestNamesakeJsonRpcConsumer.get_rpc_methods(), []) 291 | 292 | def test_jsonRpcexception_dumping(self): 293 | import json 294 | exception = JsonRpcException(1, JsonRpcConsumerTest.GENERIC_APPLICATION_ERROR, data=[True, "test"]) 295 | json_res = json.loads(str(exception)) 296 | self.assertEqual(json_res["id"], 1) 297 | self.assertEqual(json_res["jsonrpc"], "2.0") 298 | self.assertEqual(json_res["error"]["data"], [True, "test"]) 299 | self.assertEqual(json_res["error"]["code"], JsonRpcConsumerTest.GENERIC_APPLICATION_ERROR) 300 | 301 | def test_session_pass_param(self): 302 | @MyJsonRpcWebsocketConsumerTest.rpc_method() 303 | def ping_set_session(**kwargs): 304 | original_message = kwargs["original_message"] 305 | original_message.channel_session["test"] = True 306 | return "pong_set_session" 307 | 308 | @MyJsonRpcWebsocketConsumerTest.rpc_method() 309 | def ping_get_session(**kwargs): 310 | original_message = kwargs["original_message"] 311 | self.assertEqual(original_message.channel_session["test"], True) 312 | return "pong_get_session" 313 | 314 | client = HttpClient() 315 | client.send_and_consume(u'websocket.receive', 316 | text='{"id":1, "jsonrpc":"2.0", "method":"ping_set_session", "params":{}}') 317 | msg = client.receive() 318 | self.assertEqual(msg['result'], "pong_set_session") 319 | client.send_and_consume(u'websocket.receive', 320 | text='{"id":1, "jsonrpc":"2.0", "method":"ping_get_session", "params":{}}') 321 | msg = client.receive() 322 | self.assertEqual(msg['result'], "pong_get_session") 323 | 324 | def test_Session(self): 325 | 326 | @MyJsonRpcWebsocketConsumerTest.rpc_method() 327 | def ping_set_session2(**kwargs): 328 | original_message = kwargs["original_message"] 329 | original_message.channel_session["test"] = True 330 | return "pong_set_session2" 331 | 332 | @MyJsonRpcWebsocketConsumerTest.rpc_method() 333 | def ping_get_session2(**kwargs): 334 | original_message = kwargs["original_message"] 335 | self.assertNotIn("test", original_message.channel_session) 336 | return "pong_get_session2" 337 | 338 | client = HttpClient() 339 | client.send_and_consume(u'websocket.receive', 340 | text='{"id":1, "jsonrpc":"2.0", "method":"ping_set_session2", "params":{}}') 341 | msg = client.receive() 342 | self.assertEqual(msg['result'], "pong_set_session2") 343 | 344 | client2 = HttpClient() 345 | client2.send_and_consume(u'websocket.receive', 346 | text='{"id":1, "jsonrpc":"2.0", "method":"ping_get_session2", "params":{}}') 347 | msg = client2.receive() 348 | self.assertEqual(msg['result'], "pong_get_session2") 349 | 350 | def test_custom_json_encoder(self): 351 | some_date = datetime.utcnow() 352 | 353 | @MyJsonRpcWebsocketConsumerTest.rpc_method() 354 | def test_method(): 355 | return { 356 | 'date': some_date 357 | } 358 | 359 | client = HttpClient() 360 | try: 361 | client.send_and_consume(u'websocket.receive', 362 | text='{"id":1, "jsonrpc":"2.0", "method":"test_method", "params":{}}') 363 | self.fail('Looks like test does not work') 364 | except TypeError: 365 | pass 366 | 367 | @DjangoJsonRpcWebsocketConsumerTest.rpc_method() 368 | def test_method1(): 369 | return { 370 | 'date': some_date 371 | } 372 | 373 | client.send_and_consume(u'websocket.receive', 374 | text='{"id":1, "jsonrpc":"2.0", "method":"test_method1", "params":{}}', 375 | path='/django/') 376 | msg = client.receive() 377 | self.assertEqual(msg['result'], {u'date': some_date.isoformat()[:-3]}) 378 | 379 | def test_message_is_not_thread_safe(self): 380 | 381 | class SpoofMessage: 382 | 383 | class Channel: 384 | def __init__(self): 385 | self.name = None 386 | 387 | def __init__(self): 388 | self.channel = SpoofMessage.Channel() 389 | self.payload = None 390 | 391 | @MyJsonRpcWebsocketConsumerTest.rpc_method() 392 | def ping2(**kwargs): 393 | original_message = kwargs["original_message"] 394 | return original_message.payload 395 | 396 | @MyJsonRpcWebsocketConsumerTest.rpc_method() 397 | def ping3(**kwargs): 398 | original_message = kwargs["original_message"] 399 | return original_message.payload 400 | 401 | def thread_test(): 402 | for _i in range(0, 10000): 403 | _message = SpoofMessage() 404 | _message.channel.name = "websocket.test" 405 | _message.payload = "test%s" % _i 406 | _res = MyJsonRpcWebsocketConsumerTest._JsonRpcConsumer__process( 407 | {"id": 1, "jsonrpc": "2.0", "method": "ping3", "params": []}, _message) 408 | self.assertEqual(_res['result'], "test%s" % _i) 409 | 410 | import threading 411 | threading._start_new_thread(thread_test, ()) 412 | 413 | for i in range(0, 10000): 414 | _message = SpoofMessage() 415 | _message.channel.name = "websocket.test" 416 | _message.payload = "test%s" % i 417 | res = MyJsonRpcWebsocketConsumerTest._JsonRpcConsumer__process( 418 | {"id": 1, "jsonrpc": "2.0", "method": "ping2", "params": []}, _message) 419 | self.assertEqual(res['result'], "test%s" % i) 420 | 421 | def test_original_message_position_safe(self): 422 | 423 | @MyJsonRpcWebsocketConsumerTest.rpc_method() 424 | def ping_set_session(name, value, **kwargs): 425 | original_message = kwargs["original_message"] 426 | original_message.channel_session["test"] = True 427 | return ["pong_set_session", value, name] 428 | 429 | @MyJsonRpcWebsocketConsumerTest.rpc_method() 430 | def ping_get_session(value2, name2, **kwargs): 431 | original_message = kwargs["original_message"] 432 | self.assertEqual(original_message.channel_session["test"], True) 433 | return ["pong_get_session", value2, name2] 434 | 435 | client = HttpClient() 436 | client.send_and_consume(u'websocket.receive', 437 | text='{"id":1, "jsonrpc":"2.0", "method":"ping_set_session", ' 438 | '"params":["name_of_function", "value_of_function"]}') 439 | msg = client.receive() 440 | self.assertEqual(msg['result'], ["pong_set_session", "value_of_function", "name_of_function"]) 441 | client.send_and_consume(u'websocket.receive', 442 | text='{"id":1, "jsonrpc":"2.0", "method":"ping_get_session", ' 443 | '"params":{"name2": "name2_of_function", "value2": "value2_of_function"}}') 444 | msg = client.receive() 445 | self.assertEqual(msg['result'], ["pong_get_session", "value2_of_function", "name2_of_function"]) 446 | 447 | def test_websocket_param_in_decorator_for_method(self): 448 | 449 | @MyJsonRpcWebsocketConsumerTest.rpc_method(websocket=False) 450 | def ping(): 451 | return "pong" 452 | 453 | client = HttpClient() 454 | client.send_and_consume(u'websocket.receive', 455 | text='{"id":1, "jsonrpc":"2.0", "method":"ping", ' 456 | '"params":[]}') 457 | msg = client.receive() 458 | self.assertEqual(msg['error']['message'], "Method Not Found") 459 | 460 | def test_websocket_param_in_decorator_for_notification(self): 461 | 462 | @MyJsonRpcWebsocketConsumerTest.rpc_notification(websocket=False) 463 | def ping(): 464 | return "pong" 465 | 466 | client = HttpClient() 467 | client.send_and_consume(u'websocket.receive', 468 | text='{"jsonrpc":"2.0", "method":"ping", ' 469 | '"params":[]}') 470 | msg = client.receive() 471 | self.assertEqual(msg, None) 472 | 473 | 474 | class TestsNotifications(ChannelTestCase): 475 | 476 | def test_group_notifications(self): 477 | from channels import Group 478 | 479 | @MyJsonRpcWebsocketConsumerTest.rpc_method() 480 | def add_client_to_group(group_name, **kwargs): 481 | original_message = kwargs["original_message"] 482 | Group(group_name).add(original_message.reply_channel) 483 | return True 484 | 485 | @MyJsonRpcWebsocketConsumerTest.rpc_method() 486 | def send_to_group(group_name, **kwargs): 487 | MyJsonRpcWebsocketConsumerTest.notify_group(group_name, "notification.notif", {"payload": 1234}) 488 | return True 489 | 490 | @MyJsonRpcWebsocketConsumerTest.rpc_method() 491 | def send_to_reply_channel(**kwargs): 492 | original_message = kwargs["original_message"] 493 | MyJsonRpcWebsocketConsumerTest.notify_channel(original_message.reply_channel, 494 | "notification.ownnotif", 495 | {"payload": 12}) 496 | return True 497 | 498 | def send_notif(_client): 499 | _client.send_and_consume(u'websocket.receive', 500 | text='{"id":1, "jsonrpc":"2.0", "method":"send_to_group", "params":["group_test"]}') 501 | # receive notif 502 | msg = _client.receive() 503 | self.assertEqual(msg['method'], "notification.notif") 504 | self.assertEqual(msg['params'], {"payload": 1234}) 505 | 506 | # receive response 507 | msg = _client.receive() 508 | self.assertEqual(msg['result'], True) 509 | 510 | client = HttpClient() 511 | client2 = HttpClient() 512 | 513 | # we test own reply channel 514 | client.send_and_consume(u'websocket.receive', 515 | text='{"id":1, "jsonrpc":"2.0", "method":"send_to_reply_channel", "params": []}') 516 | 517 | msg = client.receive() 518 | self.assertEquals(msg['method'], "notification.ownnotif") 519 | self.assertEqual(msg['params'], {"payload": 12}) 520 | 521 | msg = client.receive() 522 | self.assertEqual(msg['result'], True) 523 | 524 | # we add client to a group_test group 525 | client.send_and_consume(u'websocket.receive', 526 | text='{"id":1, "jsonrpc":"2.0", "method":"add_client_to_group", "params":["group_test"]}') 527 | msg = client.receive() 528 | self.assertEqual(msg['result'], True) 529 | 530 | msg = client.receive() 531 | self.assertEqual(msg, None) 532 | 533 | # we make sure it works 534 | send_notif(client) 535 | 536 | # we make sure the second client didn't receive anything 537 | msg = client2.receive() 538 | self.assertEqual(msg, None) 539 | 540 | # we add the second client to another group 541 | client2.send_and_consume(u'websocket.receive', 542 | text='{"id":1, "jsonrpc":"2.0", "method":"add_client_to_group", "params":["group_test2"]}') 543 | msg = client2.receive() 544 | self.assertEqual(msg['result'], True) 545 | 546 | # send again 547 | send_notif(client) 548 | 549 | # we make sure the second client didn't receive anything 550 | msg = client2.receive() 551 | self.assertEqual(msg, None) 552 | 553 | # we add the second client to SAME group 554 | client2.send_and_consume(u'websocket.receive', 555 | text='{"id":1, "jsonrpc":"2.0", "method":"add_client_to_group", "params":["group_test"]}') 556 | msg = client2.receive() 557 | self.assertEqual(msg['result'], True) 558 | 559 | send_notif(client) 560 | 561 | # now second client should receive (as well) 562 | msg = client2.receive() 563 | self.assertEqual(msg['method'], "notification.notif") 564 | self.assertEqual(msg['params'], {"payload": 1234}) 565 | 566 | # notif from second client 567 | send_notif(client2) 568 | 569 | # now second client should receive (as well) 570 | msg = client.receive() 571 | self.assertEqual(msg['method'], "notification.notif") 572 | self.assertEqual(msg['params'], {"payload": 1234}) 573 | 574 | def test_inbound_notifications(self): 575 | 576 | @MyJsonRpcWebsocketConsumerTest.rpc_notification() 577 | def notif1(params, **kwargs): 578 | self.assertEqual(params, {"payload": True}) 579 | 580 | @MyJsonRpcWebsocketConsumerTest.rpc_notification('notif.notif2') 581 | def notif2(params, **kwargs): 582 | self.assertEqual(params, {"payload": 12345}) 583 | 584 | client = HttpClient() 585 | 586 | # we send a notification to the server 587 | client.send_and_consume(u'websocket.receive', 588 | text='{"jsonrpc":"2.0", "method":"notif1", "params":[{"payload": true}]}') 589 | msg = client.receive() 590 | self.assertEqual(msg, None) 591 | 592 | # we test with method rewriting 593 | client.send_and_consume(u'websocket.receive', 594 | text='{"jsonrpc":"2.0", "method":"notif.notif2", "params":[{"payload": 12345}]}') 595 | self.assertEqual(msg, None) 596 | 597 | def test_kwargs_not_there(self): 598 | 599 | @MyJsonRpcWebsocketConsumerTest.rpc_method() 600 | def ping(): 601 | return True 602 | 603 | client = HttpClient() 604 | 605 | # we send a notification to the server 606 | client.send_and_consume(u'websocket.receive', 607 | text='{"id":1, "jsonrpc":"2.0", "method":"ping", "params":[]}') 608 | msg = client.receive() 609 | self.assertEqual(msg["result"], True) 610 | 611 | def test_error_on_notification_frame(self): 612 | @MyJsonRpcWebsocketConsumerTest.rpc_method() 613 | def ping(): 614 | return True 615 | 616 | client = HttpClient() 617 | 618 | # we send a notification to the server 619 | client.send_and_consume(u'websocket.receive', 620 | text='{"jsonrpc":"2.0", "method":"dwqwdq", "params":[]}') 621 | msg = client.receive() 622 | self.assertEqual(msg, None) 623 | -------------------------------------------------------------------------------- /example/django_example/urls.py: -------------------------------------------------------------------------------- 1 | """django_channels_jsonrpc URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.10/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.conf.urls import url, include 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 15 | """ 16 | 17 | urlpatterns = [ 18 | 19 | ] -------------------------------------------------------------------------------- /example/django_example/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for django_channels_jsonrpc project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_example.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /example/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_example.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError: 10 | # The above import may fail for some other reason. Ensure that the 11 | # issue is really that Django is missing to avoid masking other 12 | # exceptions on Python 2. 13 | try: 14 | import django 15 | except ImportError: 16 | raise ImportError( 17 | "Couldn't import Django. Are you sure it's installed and " 18 | "available on your PYTHONPATH environment variable? Did you " 19 | "forget to activate a virtual environment?" 20 | ) 21 | raise 22 | execute_from_command_line(sys.argv) 23 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | tox==2.6.0 2 | channels==1.1.6 3 | six 4 | codeclimate-test-reporter 5 | django-cors-headers==1.2.0 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import find_packages, setup 3 | 4 | with open(os.path.join(os.path.dirname(__file__), 'README.md')) as readme: 5 | README = readme.read() 6 | 7 | # allow setup.py to be run from any path 8 | os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) 9 | 10 | setup( 11 | name='django-channels-jsonrpc', 12 | version='1.2.0', 13 | packages=find_packages(), 14 | install_requires=[ 15 | 'channels', 'django-cors-headers' 16 | ], 17 | include_package_data=True, 18 | license='MIT License', 19 | description='A JSON-RPC implementation for Django channels consumer.', 20 | long_description='Works with django channels. See README on gihub repo', 21 | url='https://github.com/millerf/django-channels-jsonrpc/', 22 | author='Fabien Millerand - MILLER/f', 23 | author_email='fab@millerf.com', 24 | test_suite='channels_jsonrpc.tests.tests', 25 | tests_require=['django', 'channels', 'django-cors-headers'], 26 | classifiers=[ 27 | 'Environment :: Web Environment', 28 | 'Framework :: Django', 29 | 'Framework :: Django :: 1.8', 30 | 'Framework :: Django :: 1.9', 31 | 'Framework :: Django :: 1.10', 32 | 'Framework :: Django :: 1.11', 33 | 'Intended Audience :: Developers', 34 | 'License :: OSI Approved :: MIT License', 35 | 'Operating System :: OS Independent', 36 | 'Programming Language :: Python', 37 | 'Programming Language :: Python :: 2', 38 | 'Programming Language :: Python :: 2.7', 39 | 'Programming Language :: Python :: 3', 40 | 'Programming Language :: Python :: 3.4', 41 | 'Programming Language :: Python :: 3.5', 42 | 'Programming Language :: Python :: 3.6', 43 | 'Topic :: Internet :: WWW/HTTP', 44 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 45 | ], 46 | ) -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | django-{18,111} 4 | 5 | [testenv] 6 | passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH 7 | commands= 8 | python --version 9 | django-admin.py test django_example 10 | setenv = 11 | DJANGO_SETTINGS_MODULE=django_example.settings 12 | PYTHONPATH={toxinidir}/example/:{toxinidir}/channels_jsonrpc/ 13 | deps = 14 | {toxinidir} 15 | django-18: Django==1.8.* 16 | django-19: Django==1.9.* 17 | django-110: Django==1.10.* 18 | django-111: Django==1.11.* 19 | channels==1.1.6 20 | django-cors-headers==1.2.0 21 | 22 | [testenv:coverage] 23 | passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH 24 | commands = 25 | python --version 26 | coverage run --branch --include=*/channels_jsonrpc/*,example/django_example/* {envbindir}/django-admin.py test django_example 27 | coverage report 28 | #coveralls 29 | setenv = 30 | DJANGO_SETTINGS_MODULE=django_example.settings 31 | PYTHONPATH={toxinidir}/example/:{toxinidir}/channels_jsonrpc/ 32 | deps = 33 | {toxinidir} 34 | coverage 35 | coveralls 36 | channels==1.1.6 37 | django-cors-headers==1.2.0 --------------------------------------------------------------------------------