├── 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 | 
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 |
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 |
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 |
--------------------------------------------------------------------------------