├── requirements.txt ├── .gitignore ├── package.json ├── templates └── index.html ├── server.py ├── README.md ├── LICENSE ├── webconsole.py └── js └── python-console.js /requirements.txt: -------------------------------------------------------------------------------- 1 | flask 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node packages 2 | node_modules/ 3 | 4 | # Bytecode Files 5 | *.py[co] 6 | 7 | # Virtualenv 8 | env/ 9 | 10 | # Generated Files 11 | static/app.js 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "python-browser-console", 3 | "version": "0.1.0", 4 | "dependencies": { 5 | "react": "~0.12.1", 6 | "jquery-browserify": "~1.8.1", 7 | "underscore": "*", 8 | "reactify": "~1.0.0", 9 | "react-bootstrap": "~0.13.3" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Hello React 5 | 6 | 7 | 8 | 9 |
10 |

Python Browser Console

11 |
12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | import flask 2 | import json 3 | import cgi 4 | import webconsole 5 | 6 | app = flask.Flask(__name__) 7 | console = webconsole.WebConsoleInterpreter() 8 | 9 | 10 | def _get_console_state_json(): 11 | return json.dumps({ 12 | "outputRecords": list(console.get_output_lines()), 13 | "history": console.get_history(), 14 | }) 15 | 16 | 17 | @app.route("/") 18 | def index(): 19 | return flask.render_template("index.html") 20 | 21 | 22 | @app.route("/api/console", methods=['GET', 'POST']) 23 | def post_to_console(): 24 | inp = flask.request.form.get('input', None) 25 | if inp is not None: 26 | console.push(inp) 27 | 28 | return _get_console_state_json() 29 | 30 | if __name__ == "__main__": 31 | app.run(debug=True) 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-browser-console 2 | 3 | Get an interactive python console to a live, running app easily. 4 | 5 | ## Installation 6 | 7 | After cloning the repo, you will need to do the following to run the 8 | application: 9 | 10 | $ sudo apt-get update 11 | $ sudo apt-get install python-virtualenv nodejs 12 | $ virtualenv env 13 | $ source env/bin/activate 14 | $ pip install -i requirements.txt 15 | $ sudo npm install -g browserify 16 | $ npm install 17 | $ browserify -t reactify -o static/app.js js/*.js 18 | $ python server.py 19 | 20 | Then go to http://localhost:5000/ 21 | 22 | Whenever changes are made to the source javascript, you will need to 23 | re-run `browserify -t reactify -o static/app.js js/*.js` and reload 24 | your web page. 25 | 26 | ## Preview 27 | 28 | Here's a screenshot of the end result: 29 | 30 | ![Screenshot](http://i.imgur.com/10WjB57.png) 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Paul Osborne 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 | -------------------------------------------------------------------------------- /webconsole.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import code 3 | import sys 4 | import StringIO 5 | 6 | 7 | class WebConsoleInterpreter(object): 8 | 9 | def __init__(self, output_history=1000, command_history=100): 10 | self._output_idx = 0 11 | self._output = collections.deque(maxlen=output_history) 12 | self._history = collections.deque(maxlen=command_history) 13 | self.interpreter = code.InteractiveConsole(globals()) 14 | 15 | def _get_idx(self): 16 | idx = self._output_idx 17 | self._output_idx += 1 18 | return idx 19 | 20 | def get_history(self): 21 | return list(self._history) 22 | 23 | def get_output_lines(self, since_idx=0): 24 | for record in list(self._output): 25 | if record["idx"] >= since_idx: 26 | yield record 27 | 28 | def get_output(self, since_idx=0): 29 | sio = StringIO.StringIO() 30 | for record in self.get_output_lines(since_idx): 31 | if record["type"] == "input": 32 | sio.write(">>> {}\n".format(record["line"])) 33 | else: 34 | sio.write("{}\n".format(record["line"])) 35 | return sio.getvalue() 36 | 37 | def push(self, line): 38 | output = StringIO.StringIO() 39 | orig_stdout = sys.stdout 40 | orig_stderr = sys.stderr 41 | sys.stdout = sys.stderr = output 42 | self.interpreter.push(line) 43 | sys.stdout = orig_stdout 44 | sys.stderr = orig_stderr 45 | 46 | # record the results 47 | self._history.append(line) 48 | self._output.append({ 49 | "type": "input", 50 | "idx": self._get_idx(), 51 | "line": line 52 | }) 53 | 54 | output.seek(0) # move back to beginning of buffer 55 | for line in output.read().split('\n'): 56 | idx = self._output_idx 57 | self._output_idx += 1 58 | self._output.append({ 59 | "type": "output", 60 | "idx": self._get_idx(), 61 | "line": line.strip("\r\n"), 62 | }) 63 | 64 | if __name__ == "__main__": 65 | console = WebConsoleInterpreter() 66 | console.push("print 'hello'") 67 | console.push("print 'world'") 68 | print console.get_output() 69 | -------------------------------------------------------------------------------- /js/python-console.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery-browserify'); 2 | var React = require('react'); 3 | var BSInput = require('react-bootstrap').Input; 4 | 5 | var PythonConsole = React.createClass({ 6 | getInitialState: function() { 7 | return { 8 | history: [], 9 | outputRecords: [], 10 | } 11 | }, 12 | submitInput: function(input) { 13 | $.ajax({ 14 | url: '/api/console', 15 | dataType: 'json', 16 | type: 'POST', 17 | data: {input: input}, 18 | success: function(data) { 19 | console.log(data); 20 | this.setState({ 21 | outputRecords: data.outputRecords, 22 | history: data.history, 23 | }); 24 | }.bind(this), 25 | error: function(xhr, status, err) { 26 | console.error("/api/console", status, err.toString()); 27 | }.bind(this) 28 | }); 29 | }, 30 | render: function() { 31 | return ( 32 |
33 | 36 | 37 |
38 | ); 39 | } 40 | }); 41 | 42 | var PythonConsoleOutput = React.createClass({ 43 | componentDidUpdate: function() { 44 | // on update, we need to tell the scrollbar to scroll to 45 | // to bottom. 46 | var node = this.getDOMNode(); 47 | node.scrollTop = node.scrollHeight; 48 | }, 49 | render: function() { 50 | var output = []; 51 | for (var i = 0; i < this.props.outputRecords.length; i++) { 52 | var record = this.props.outputRecords[i]; 53 | console.log(record.line); 54 | if (record.type == "input") { 55 | output.push(>>> {record.line}); 56 | } else if (record.type == "output") { 57 | output.push(
{record.line}
); 58 | } 59 | } 60 | 61 | var codeStyle = { 62 | "white-space": "pre-wrap", 63 | "font-family": "monospace", 64 | "height": this.props.outputHeight, 65 | "overflow": "auto", 66 | "top": 0, 67 | }; 68 | 69 | return ( 70 |
{output}
71 | ); 72 | } 73 | }); 74 | 75 | var PythonConsoleInput = React.createClass({ 76 | handleSubmit: function(e) { 77 | e.preventDefault(); 78 | var inputDOMNode = this.refs.input.getInputDOMNode(); 79 | this.props.submitInput(inputDOMNode.value); 80 | inputDOMNode.value = ''; 81 | }, 82 | render: function() { 83 | var inputBoxStyle = { 84 | "font-family": "monospace" 85 | }; 86 | return ( 87 |
88 |
89 |
90 | 96 |
97 |
98 |
99 |
100 | ); 101 | } 102 | }); 103 | 104 | /* Render the python console to the page */ 105 | $(document).ready(function() { 106 | console.log("Rendering!"); 107 | React.render( 108 | , 109 | document.getElementById('browser-console') 110 | ); 111 | }); 112 | --------------------------------------------------------------------------------