├── methods.py
├── .gitignore
├── index.html
├── LICENSE.txt
├── style.css
├── README.md
├── server.py
└── client.js
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------