├── .gitignore ├── LICENSE.txt ├── README.md ├── client.js ├── index.html ├── methods.py ├── server.py └── style.css /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 2013 Patrick Fuller, patrick-fuller.com 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Python-Javascript Communication 2 | =============================== 3 | 4 | An example of using the [Tornado Web Server](http://www.tornadoweb.org/en/stable/) 5 | enable running Python (as a "server") through a website (the "client") with 6 | Javascript. 7 | 8 | This example uses [websockets](http://en.wikipedia.org/wiki/WebSocket), a way 9 | to create a "bridge" between the Python server and Javascript client. It has 10 | multiple advantages over HTTP-based communication, and is especially preferable 11 | in situations where you can tell your users to not use Internet Explorer. 12 | 13 | This code provides enough infrastructure to write your own methods by appending 14 | files. Any function added to `methods.py` can be called from the client by 15 | making a corresponding function in `static/client.js`, as well as some basic 16 | logic to handle the server response. Follow the setup of the "count" function 17 | to get this running. 18 | 19 | There is also basic error handling - stack traces from the server are sent back 20 | to the client and displayed in an alert window. 21 | 22 | Dependencies 23 | ------------ 24 | 25 | This program uses Tornado to handle the mess of communication. 26 | 27 | ``` 28 | pip install tornado 29 | ``` 30 | 31 | Usage 32 | ----- 33 | 34 | Once the dependencies are met, just run 35 | 36 | ``` 37 | python server.py 38 | ``` 39 | 40 | and then open http://localhost:8000/ in a browser. The example uses server-side 41 | python to count numbers. 42 | -------------------------------------------------------------------------------- /client.js: -------------------------------------------------------------------------------- 1 | /*global WebSocket, JSON, $, window, console, alert*/ 2 | "use strict"; 3 | /** 4 | * Function calls across the background TCP socket. Uses JSON RPC + a queue. 5 | * (I've added this extra logic to simplify expanding this) 6 | */ 7 | var client = { 8 | queue: {}, 9 | 10 | // Connects to Python through the websocket 11 | connect: function (port) { 12 | var self = this; 13 | this.socket = new WebSocket("ws://" + window.location.hostname + ":" + port + "/websocket"); 14 | 15 | this.socket.onopen = function () { 16 | console.log("Connected!"); 17 | }; 18 | 19 | this.socket.onmessage = function (messageEvent) { 20 | var router, current, updated, jsonRpc; 21 | 22 | jsonRpc = JSON.parse(messageEvent.data); 23 | router = self.queue[jsonRpc.id]; 24 | delete self.queue[jsonRpc.id]; 25 | self.result = jsonRpc.result; 26 | 27 | // If there's an error, display it in an alert window 28 | if (jsonRpc.error) { 29 | alert(jsonRpc.result); 30 | 31 | // If the response is from "count", do stuff 32 | } else if (router === "count") { 33 | current = $(".answer").html(); 34 | if (current.length === 0) { 35 | updated = jsonRpc.result; 36 | } else if (current.length > 100) { 37 | updated = current.substring(current.length - 100) + ", " + jsonRpc.result; 38 | } else { 39 | updated = current + ", " + jsonRpc.result; 40 | } 41 | $(".answer").html(updated); 42 | $(".number").val(jsonRpc.result); 43 | 44 | // If the response is from anything else, it's currently unsupported 45 | } else { 46 | alert("Unsupported function: " + router); 47 | } 48 | }; 49 | }, 50 | 51 | // Generates a unique identifier for request ids 52 | // Code from http://stackoverflow.com/questions/105034/ 53 | // how-to-create-a-guid-uuid-in-javascript/2117523#2117523 54 | uuid: function () { 55 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 56 | var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); 57 | return v.toString(16); 58 | }); 59 | }, 60 | 61 | // Placeholder function. It adds one to things. 62 | count: function (data) { 63 | var uuid = this.uuid(); 64 | this.socket.send(JSON.stringify({method: "count", id: uuid, params: {number: data}})); 65 | this.queue[uuid] = "count"; 66 | } 67 | }; 68 | 69 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | python-js bridge 4 | 5 | 6 | 7 | 8 | 9 | 10 |

Python-Javascript communication

11 |

Counting is hard. Let Python do the heavy lifting for you.

12 | 13 | 14 |

history

15 |
16 | 17 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /methods.py: -------------------------------------------------------------------------------- 1 | """ 2 | An entire file for you to expand. Add methods here, and the client should be 3 | able to call them with json-rpc without any editing to the pipeline. 4 | """ 5 | 6 | 7 | def count(number): 8 | """It counts. Duh. Note: intentionally written to break on non-ints""" 9 | return int(number) + 1 10 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | """ 2 | Creates an HTTP server with basic websocket communication. 3 | """ 4 | import argparse 5 | import json 6 | import os 7 | import traceback 8 | import webbrowser 9 | 10 | import tornado.web 11 | import tornado.websocket 12 | 13 | import methods 14 | 15 | 16 | class IndexHandler(tornado.web.RequestHandler): 17 | 18 | def get(self): 19 | self.render("index.html", port=args.port) 20 | 21 | 22 | class WebSocket(tornado.websocket.WebSocketHandler): 23 | 24 | def on_message(self, message): 25 | """Evaluates the function pointed to by json-rpc.""" 26 | json_rpc = json.loads(message) 27 | 28 | try: 29 | # The only available method is `count`, but I'm generalizing 30 | # to allow other methods without too much extra code 31 | result = getattr(methods, 32 | json_rpc["method"])(**json_rpc["params"]) 33 | error = None 34 | except: 35 | # Errors are handled by enabling the `error` flag and returning a 36 | # stack trace. The client can do with it what it will. 37 | result = traceback.format_exc() 38 | error = 1 39 | 40 | self.write_message(json.dumps({"result": result, "error": error, 41 | "id": json_rpc["id"]}, 42 | separators=(",", ":"))) 43 | 44 | 45 | parser = argparse.ArgumentParser(description="Starts a webserver for stuff.") 46 | parser.add_argument("--port", type=int, default=8000, help="The port on which " 47 | "to serve the website.") 48 | args = parser.parse_args() 49 | 50 | handlers = [(r"/", IndexHandler), (r"/websocket", WebSocket), 51 | (r'/static/(.*)', tornado.web.StaticFileHandler, 52 | {'path': os.path.normpath(os.path.dirname(__file__))})] 53 | application = tornado.web.Application(handlers) 54 | application.listen(args.port) 55 | 56 | webbrowser.open("http://localhost:%d/" % args.port, new=2) 57 | 58 | tornado.ioloop.IOLoop.instance().start() 59 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 30px; 3 | padding: 0; 4 | } 5 | 6 | html, body { 7 | height: 100%; 8 | overflow: hidden; 9 | } 10 | 11 | body { 12 | background: #eee; 13 | width: 100%; 14 | height: 100%; 15 | overflow-x: hidden; 16 | 17 | font-family: 'Lato', Helvetica, sans-serif; 18 | font-size: 16px; 19 | color: #222; 20 | 21 | -webkit-overflow-scrolling: touch; 22 | 23 | -webkit-box-sizing: border-box; 24 | -moz-box-sizing: border-box; 25 | box-sizing: border-box; 26 | } 27 | body p { 28 | font-size: 16px; 29 | line-height: 1.32; 30 | } 31 | 32 | input, 33 | button { 34 | height: 35px; 35 | width: 150px; 36 | font-size: 15px; 37 | margin-top: 0px; 38 | } 39 | 40 | button { 41 | border: 0px; 42 | padding: 8px 10px; 43 | border-radius: 1px; 44 | 45 | cursor: pointer; 46 | color: #fff; 47 | background: #7aa76d; 48 | text-align: left; 49 | 50 | -webkit-transition: 0.15s background ease; 51 | -moz-transition: 0.15s background ease; 52 | -ms-transition: 0.15s background ease; 53 | -o-transition: 0.15s background ease; 54 | transition: 0.15s background ease; 55 | } 56 | button:hover { 57 | background: #91cd85; 58 | } 59 | button:active { 60 | background: #60895a; 61 | } 62 | 63 | h3 { 64 | margin-bottom: 5px; 65 | } 66 | .answer { 67 | margin-top: 0px; 68 | } 69 | --------------------------------------------------------------------------------