├── .gitmodules ├── doc ├── img │ ├── minimal.gif │ ├── nppwad.gif │ ├── nppwad2.gif │ └── serverless.gif ├── example_wsgi.py └── AutoReload.md ├── common.py ├── .gitattributes ├── License.md ├── .gitignore ├── serverless.py ├── client.py ├── server.py ├── README.md ├── minimal_allinone.py └── allinone.py /.gitmodules: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/img/minimal.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Michael-F-Ellis/NearlyPurePythonWebAppDemo/HEAD/doc/img/minimal.gif -------------------------------------------------------------------------------- /doc/img/nppwad.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Michael-F-Ellis/NearlyPurePythonWebAppDemo/HEAD/doc/img/nppwad.gif -------------------------------------------------------------------------------- /doc/img/nppwad2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Michael-F-Ellis/NearlyPurePythonWebAppDemo/HEAD/doc/img/nppwad2.gif -------------------------------------------------------------------------------- /doc/img/serverless.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Michael-F-Ellis/NearlyPurePythonWebAppDemo/HEAD/doc/img/serverless.gif -------------------------------------------------------------------------------- /common.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Description: Information needed on both client and server side. 4 | 5 | This file is part of NearlyPurePythonWebAppDemo 6 | https://github.com/Michael-F-Ellis/NearlyPurePythonWebAppDemo 7 | 8 | Author: Mike Ellis 9 | Copyright 2017 Ellis & Grant, Inc. 10 | License: MIT License 11 | """ 12 | ## The number of state items to display in the web page. 13 | nitems = 10 14 | ## Enumerated names for each item. 15 | statekeys = ["item{}".format(n) for n in range(nitems)] 16 | ## Initial step size for random walk 17 | stepsize = 0.5 18 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | * text=auto 3 | 4 | # Explicitly declare text files you want to always be normalized and converted 5 | # to native line endings on checkout. 6 | *.c text 7 | *.css text 8 | *.example text 9 | *.h text text 10 | *.html text 11 | *.ics text 12 | *.ini text 13 | *.js text 14 | *.json text 15 | *.jsonp text 16 | *.load text 17 | *.md text 18 | *.map text 19 | *.markdown text 20 | *.py text 21 | *.local text 22 | *.sh text 23 | *.xml text 24 | rc.local text 25 | 26 | # Declare files that will always have CRLF line endings on checkout. 27 | # *.sln text eol=crlf 28 | 29 | # Denote all files that are truly binary and should not be modified. 30 | *.png binary 31 | *.jpg binary 32 | *.ico 33 | *.pdf binary 34 | *.woff binary 35 | *.woff2 binary 36 | *.svg binary 37 | *.sqlite binary 38 | *.ttf binary 39 | *.eot binary 40 | *.table binary 41 | 42 | -------------------------------------------------------------------------------- /License.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 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 | -------------------------------------------------------------------------------- /doc/example_wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example wsgi python file for pythonAnywhere hosting. 3 | 4 | # This file contains the WSGI configuration required to serve up your 5 | # web application at http://YourSubdomain.pythonanywhere.com/ 6 | # It works by setting the variable 'application' to a WSGI handler of some 7 | # description. 8 | 9 | # +++++++++++ GENERAL DEBUGGING TIPS +++++++++++ 10 | # getting imports and sys.path right can be fiddly! 11 | # We've tried to collect some general tips here: 12 | # https://www.pythonanywhere.com/wiki/DebuggingImportError 13 | 14 | # +++++++++++ VIRTUALENV +++++++++++ 15 | # If you want to use a virtualenv, set its path on the web app setup tab. 16 | # Then come back here and import your application object as per the 17 | # instructions below. 18 | 19 | # +++++++++++ WORKING DIRECTORY 20 | # Also, if the application needs to access files for, 21 | # say, rebuilding sources then your need to set the working directory 22 | # accordingly on the web app setup tab. 23 | """ 24 | 25 | # +++++++++++ CUSTOM WSGI +++++++++++ 26 | # If you have a WSGI file that you want to serve using PythonAnywhere, perhaps 27 | # in your home directory under version control, then use something like this: 28 | # 29 | import sys 30 | path = '/home/YourSubdomain/path/to/NearlyPurePythonWebAppDemo' 31 | if path not in sys.path: 32 | sys.path.append(path) 33 | 34 | # Import the wrapped Bottle app object. 35 | from server import app_for_wsgi_env as application 36 | 37 | 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## File types that should not be version controlled. 2 | 3 | # Compiled source # 4 | ################### 5 | *.com 6 | *.class 7 | *.dll 8 | *.exe 9 | *.o 10 | *.so 11 | 12 | # Packages # 13 | ############ 14 | # it's better to unpack these files and commit the raw source 15 | # git has its own built in compression methods 16 | *.7z 17 | *.dmg 18 | *.gz 19 | *.iso 20 | *.jar 21 | *.rar 22 | *.tar 23 | *.zip 24 | 25 | # Logs and databases # 26 | ###################### 27 | *.db 28 | *.log 29 | *.log.* 30 | *.sql 31 | *.sqlite 32 | *.out 33 | 34 | 35 | # OS generated files # 36 | ###################### 37 | .DS_Store 38 | .DS_Store? 39 | ._* 40 | .Spotlight-V100 41 | .Trashes 42 | ehthumbs.db 43 | Thumbs.db 44 | 45 | # Editor temporary files # 46 | ########################## 47 | *.swo 48 | *.swp 49 | *.bak 50 | 51 | 52 | # UltiSnips snippet files # 53 | ########################### 54 | *.snippets 55 | 56 | ## Python specific files 57 | 58 | # Byte-compiled / optimized / DLL files 59 | __pycache__/ 60 | *.py[cod] 61 | *$py.class 62 | 63 | # C extensions 64 | *.so 65 | 66 | # Distribution / packaging 67 | .Python 68 | env/ 69 | build/ 70 | develop-eggs/ 71 | dist/ 72 | downloads/ 73 | eggs/ 74 | .eggs/ 75 | lib/ 76 | lib64/ 77 | parts/ 78 | sdist/ 79 | var/ 80 | *.egg-info/ 81 | .installed.cfg 82 | *.egg 83 | 84 | # PyInstaller 85 | # Usually these files are written by a python script from a template 86 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 87 | *.manifest 88 | *.spec 89 | 90 | # Installer logs 91 | pip-log.txt 92 | pip-delete-this-directory.txt 93 | 94 | # Unit test / coverage reports 95 | htmlcov/ 96 | .tox/ 97 | .coverage 98 | .coverage.* 99 | .cache 100 | nosetests.xml 101 | coverage.xml 102 | *,cover 103 | .hypothesis/ 104 | 105 | # Translations 106 | *.mo 107 | *.pot 108 | 109 | # Django stuff: 110 | *.log 111 | local_settings.py 112 | 113 | # Flask stuff: 114 | instance/ 115 | .webassets-cache 116 | 117 | # Scrapy stuff: 118 | .scrapy 119 | 120 | # Sphinx documentation 121 | docs/_build/ 122 | 123 | # PyBuilder 124 | target/ 125 | 126 | # IPython Notebook 127 | .ipynb_checkpoints 128 | 129 | # pyenv 130 | .python-version 131 | 132 | # celery beat schedule file 133 | celerybeat-schedule 134 | 135 | # dotenv 136 | .env 137 | 138 | # virtualenv 139 | venv/ 140 | ENV/ 141 | 142 | # Spyder project settings 143 | .spyderproject 144 | 145 | # Rope project settings 146 | .ropeproject 147 | 148 | # NearlyPurePythonWebAppDemo directories for generated files 149 | __javascript__/ 150 | __pycache__/ 151 | __html__/ 152 | 153 | 154 | -------------------------------------------------------------------------------- /doc/AutoReload.md: -------------------------------------------------------------------------------- 1 | # Auto Reload 2 | 3 | Other things being equal, the rate of progress in application development is directly related to the time it takes to make and test one small change. By default, the NearlyPurePythonWebAppDemo (NPPWAD hereafter) skeleton will detect changes in any of the Python source files and reload both the server and the client side -- including a rebuild of `client.js` from `client.py` if needed. 4 | 5 | *Note: The mechanism described here is also incorporated in the new single-file skeleton, `allinone.py`.* 6 | ## How it works 7 | 8 | Bottle already provides an auto-reloader and a debug mode. The Bottle Tutorial explains both of those [here](https://bottlepy.org/docs/dev/tutorial.html#debug-mode). NPPWAD takes advantage of them and extends them by 9 | 10 | 1. Incorporating a simple rebuild facility in `server.py,` and 11 | 2. Running a client-side check on server start time in `client.py`. 12 | 13 | The result is that changing any of the .py source files triggers the following chain of events: 14 | 15 | * Bottle notices that a file has changed and reloads `server.py.` 16 | * As `server.py` re-initializes it checks the modification times on the sources and regenerates `__html__/index.html` and `client.js` as needed. 17 | * If a prior version of `client.js` is running in a browser, the next state update will detect a change in `_state['server_start_time']` and reload the page using `location.reload()`. 18 | 19 | ## Usage 20 | Reload and debug are enabled by default when you launch from the command line. 21 | 22 | To disable these features in a production version, launch `server.py` with `--no-reloader --no-debug` at the command line. If launching by direct call to `server.serve()`, note that reload and debug are *disabled* by default in the keyword args to `serve()`. 23 | 24 | 25 | 26 | ## Limitations and caveats 27 | 28 | The reloader in Bottle inspects `sys.modules` to determine what files to monitor -- meaning that it only tracks modules that are in the tree of modules imported into `server.py.` To monitor changes in client-side Python sources, e.g. `client.py`, we must import them into `server.py` -- which means that such modules must not cause errors or have undesired side effects when imported into Python code. Notice, for example, at the bottom of `client.py` the lines 29 | 30 | ``` 31 | try: 32 | document.addEventListener('DOMContentLoaded', start) 33 | except NameError: 34 | pass 35 | ``` 36 | This prevents a failed import when `document`, a global object in JS, is not defined in Python. Fortunately, the event-driven nature of client-side code makes it easy to confine module-level execution to a single statement, as above, to avoid littering the code with many similar guard constructs. 37 | 38 | Another obvious limitation is that the reloader only monitors Python sources. For a large project with sources in other languages, you would probably want to set up a build system triggered from a monitor program such as `entr`. To connect it with the `server.py` reloader, you could arrange for the build system to update a dummy python module, e.g. `reloadme.py` imported by `server.py` for the sole purpose of detecting the need for a reload. 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /serverless.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | A once-through script that creates and loads HTML and Javascript. 4 | Uses Transcrypt and CPython's webbrowser module. There are 3 sections: 5 | 6 | Common Code -- used by both client and server 7 | Server Code -- Specific to the web service 8 | Client Code -- Compiled to JS by Transcrypt 9 | 10 | You can search for the above names in your editor for convenient navigation. 11 | 12 | External dependencies: 13 | Python >= 3.5 (Available from www.python.org) 14 | pip install transcrypt 15 | pip install htmltree 16 | 17 | USAGE: 18 | Typically: 19 | $ python serverless.py 20 | 21 | Author: Mike Ellis 22 | Copyright 2017 Ellis & Grant, Inc 23 | License: MIT License 24 | 25 | This file is part of NearlyPurePythonWebAppDemo 26 | https://github.com/Michael-F-Ellis/NearlyPurePythonWebAppDemo 27 | """ 28 | ####################################################################### 29 | ## Common Code 30 | ## Visible to CPython and Transcrypt 31 | ####################################################################### 32 | from htmltree.htmltree import * 33 | 34 | ## If working from a renamed copy of this module, change the next 35 | ## line accordingly. 36 | _module_basename = 'serverless' 37 | _module_pyname = _module_basename + '.py' 38 | _module_jsname = _module_basename + '.js' 39 | 40 | ## A little bit of trickiness here. We need to cause CPython to see only 41 | ## what's intended to be server-side code and Transcrypt to see only what's 42 | ## intended be client-side code. The solution is to try to invoke the Transcrypt 43 | ## pragma function. It won't be defined in CPython, so the server code can 44 | ## go in the 'except' branch (wrapped in skip/noskip pragmas in comments) 45 | ## and the client code follows in an 'else' branch. 46 | try: 47 | __pragma__('js', "") 48 | except NameError: 49 | ####################################################################### 50 | ## Server Code 51 | ####################################################################### 52 | #__pragma__('skip') 53 | 54 | import subprocess 55 | import webbrowser 56 | 57 | def buildIndexHtml(): 58 | """ 59 | Create the content index.html file. For the purposes of the demo, we 60 | create it with an empty body element to be filled in on the client 61 | side. Returns file URL. 62 | """ 63 | viewport = Meta(name='viewport', content='width=device-width, initial-scale=1') 64 | 65 | head = Head(viewport, 66 | Script(src='../__javascript__/' + _module_jsname, 67 | charset='UTF-8')) 68 | 69 | body = Body() 70 | 71 | doc = Html(head, body) 72 | print(doc.render(0)) 73 | return doc.renderToFile('__html__/index.html', 0) 74 | 75 | 76 | ## Create the HTML 77 | indexurl = buildIndexHtml() 78 | print(indexurl) 79 | 80 | ## Transpile to client code 81 | proc = subprocess.Popen('transcrypt -b -n -m {}'.format(_module_pyname), shell=True) 82 | if proc.wait() != 0: 83 | raise Exception("Failed trying to build {}".format(_module_jsname)) 84 | 85 | ## Open in default web browser. 86 | webbrowser.open(indexurl) 87 | 88 | #__pragma__('noskip') 89 | ## ------------- End of Server Code ----------------------------------- 90 | 91 | pass ## needed so the except branch looks complete to Transcrypt. 92 | else: 93 | 94 | ####################################################################### 95 | ## Client Code 96 | ## Compiled by Transcrypt 97 | ####################################################################### 98 | 99 | 100 | def makeBody(): 101 | """ 102 | Create HTML for the body element content. This is done as a demo to show 103 | that the code in htmltree.py works in Transcrypted JS as well as in Python. 104 | It could have been accomplished just as easily on the server side. 105 | 106 | Uses JS: .innerHTML, .style 107 | """ 108 | document.body.style.backgroundColor = "black" 109 | banner = H1("Hello, {}.".format(_module_basename), style=dict(color='yellow')) 110 | counter = H2(id="counter", style=dict(color='green')) 111 | header = Div(banner, counter, style=dict(text_align='center')) 112 | bodycontent = Div(header) 113 | 114 | ## Use the DOM API to insert rendered content 115 | document.body.innerHTML = bodycontent.render() 116 | 117 | 118 | 119 | _updater = None ## Initialized below as a generator instance. 120 | 121 | def start (): 122 | """ 123 | Client-side app execution starts here. 124 | 125 | Uses JS: .getElementById, .addEventListener, 126 | .textContent, .hasOwnProperty, .setInterval 127 | """ 128 | ## Create the body content 129 | makeBody() 130 | 131 | ## Counter update generator ## 132 | def update_counter(e): 133 | counter = document.getElementById('counter') 134 | count = 0 135 | while True: 136 | count += 1 137 | counter.textContent = "{}".format(count) 138 | yield 139 | 140 | global _updater 141 | _updater = update_counter() 142 | 143 | ## define polling function 144 | def update (): 145 | next(_updater) 146 | 147 | ## First update 148 | update () 149 | ## Repeat every second. 150 | window.setInterval (update, 1000) 151 | 152 | ## Wait until the DOM is loaded before calling start() 153 | document.addEventListener('DOMContentLoaded', start) 154 | 155 | ## ------------- End of Client Code ----------------------------------- 156 | -------------------------------------------------------------------------------- /client.py: -------------------------------------------------------------------------------- 1 | 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Description: Client-side code that gets transpiled to JS by Transcrypt(TM) 5 | Execution begins in the start() method -- which is invoked from a script element 6 | in the html head element of index.html 7 | 8 | This file is part of NearlyPurePythonWebAppDemo 9 | https://github.com/Michael-F-Ellis/NearlyPurePythonWebAppDemo 10 | 11 | Author: Mike Ellis 12 | Copyright 2017 Ellis & Grant, Inc. 13 | License: MIT License 14 | """ 15 | import common 16 | from htmltree.htmltree import * 17 | 18 | _state = {} 19 | _prior_state = {} 20 | _readouts = None 21 | 22 | def makeBody(): 23 | """ 24 | Create HTML for the body element content. This is done as a demo to show 25 | that the code in htmltree.py works in Transcrypted JS as well as in Python. 26 | It could have been accomplished just as easily on the server side. 27 | """ 28 | banner = H1("Nearly Pure Python Web App Demo", style=dict(color='yellow')) 29 | projectlink = A('Source Code on GitHub', 30 | href='https://github.com/Michael-F-Ellis/NearlyPurePythonWebAppDemo') 31 | subbanner = H2(projectlink) 32 | 33 | header = Div(banner, subbanner, style=dict(text_align='center')) 34 | 35 | readouts = [] 36 | for datakey in common.statekeys: 37 | readouts.append(Div('waiting ...', _class='readout', data_key=datakey)) 38 | 39 | stepinput = Label("Step Size", 40 | Input(id='stepinput', type='text', style=dict(margin='1em')), 41 | style=dict(color='white')) 42 | 43 | stepsubmit = Input(type="submit", value="Submit") 44 | 45 | stepform = Form( 46 | Div(stepinput, stepsubmit, style=dict(margin='20px')), 47 | id='setstep') 48 | 49 | bodycontent = Div(header) 50 | bodycontent.C.extend(readouts) 51 | bodycontent.C.append(stepform) 52 | 53 | ## Use the DOM API to insert rendered content 54 | console.log(bodycontent.render()) 55 | document.body.innerHTML = bodycontent.render() 56 | 57 | ######################################################### 58 | # jQuery replacement functions 59 | # The next 3 function could logically be placed in a 60 | # separate module. 61 | ######################################################### 62 | def triggerCustomEvent(name, data): 63 | """ 64 | JS version of jQuery.trigger. 65 | see http://youmightnotneedjquery.com/#trigger_custom 66 | """ 67 | if window.CustomEvent: 68 | event = __new__ (CustomEvent(name, {'detail' : data})) 69 | else: 70 | event = document.createEvent('CustomEvent') 71 | event.initCustomEvent(name, True, True, data) 72 | document.dispatchEvent(event) 73 | 74 | def getJSON(url, f): 75 | """ 76 | JS version of jQuery.getJSON 77 | see http://youmightnotneedjquery.com/#get_json 78 | url must return a JSON string 79 | f(data) handles an object parsed from the return JSON string 80 | """ 81 | request = __new__ (XMLHttpRequest()) 82 | request.open('GET', url, True) 83 | def onload(): 84 | if 200 <= request.status < 400: 85 | data = JSON.parse(request.responseText) 86 | f(data) ## call handler with object created from JSON string 87 | else: 88 | _ = "Server returned {} for getJSON request on {}".format(request.status, url) 89 | console.log(_) 90 | def onerror(): 91 | _ = "Connection error for getJSON request on {}".format(url) 92 | console.log(_) 93 | request.onload = onload 94 | request.onerror = onerror 95 | request.send() 96 | 97 | def post(url, data): 98 | """ 99 | JS version of jQuery.post 100 | see http://youmightnotneedjquery.com/#post 101 | data is expected to be a dict. 102 | """ 103 | request = __new__(XMLHttpRequest()) 104 | request.open('POST', url, True) 105 | request.setRequestHeader('Content-Type', 106 | 'application/x-www-form-urlencoded; ' 107 | 'charset=UTF-8') 108 | ## serialize the data, see http://stackoverflow.com/a/1714899/426853 109 | ldata = [] 110 | for k,v in data.items(): 111 | if data.hasOwnProperty(k): 112 | lh = encodeURIComponent(k) 113 | rh = encodeURIComponent(v) 114 | ldata.append("{}={}".format(lh, rh)) 115 | 116 | request.send("&".join(ldata)) 117 | 118 | # End of j!uery replacement functions 119 | ######################################################## 120 | 121 | def getState(): 122 | """ Fetch JSON obj containing monitored variables. """ 123 | def f(data): 124 | global _state, _prior_state 125 | _prior_state.update(_state) 126 | _state = data 127 | triggerCustomEvent('state:update', {}) 128 | #console.log(_state) 129 | getJSON('/getstate', f) 130 | return 131 | 132 | def update_readouts(): 133 | """ 134 | Triggered on each readout by 'state:update' custom event. We check each 135 | state value and alter it's text color accordingly. 136 | """ 137 | ## queue the new values and colora 138 | queue = [] 139 | for el in _readouts: 140 | key = el.getAttribute('data-key') 141 | value = _state[key] 142 | valuef = float(value) 143 | if valuef <= 2.0: 144 | color = 'deepskyblue' 145 | elif valuef >= 8.0: 146 | color = 'red' 147 | else: 148 | color = 'green' 149 | queue.append((el, value, color)) 150 | 151 | ## write them to the DOM 152 | for el, value, color in queue: 153 | el.textContent = value 154 | el.setAttribute('style', "color:{}; font-size:32;".format(color)) 155 | 156 | ## Also update the stepsize input with the current value, but 157 | ## check that the element does not have focus before doing so 158 | ## tp prevent update while user is typing. 159 | inp = document.getElementById('stepinput') 160 | if inp != document.activeElement: 161 | inp.value = _state['stepsize'] 162 | 163 | 164 | def handle_stepchange(event): 165 | """ 166 | Check that the request for a new step size is a number between 0 and 10 167 | before allowing the submit action to proceed. 168 | """ 169 | fail_msg = "Step size must be a number between 0 and 10" 170 | v = document.getElementById('stepinput').value 171 | # Transcrypt float() is buggy, so use some inline JS. 172 | # See https://github.com/QQuick/Transcrypt/issues/314 173 | #__pragma__('js','{}','var vj = parseFloat(v); var isfloat = !isNaN(vj);') 174 | if isfloat and (0.0 <= vj <= 10.0): 175 | ## It's valid. Send it. 176 | post('/setstepsize', { 'stepsize': v }) 177 | return False 178 | else: 179 | alert(fail_msg) 180 | return False 181 | 182 | def start (): 183 | """ 184 | Client-side app execution starts here. 185 | """ 186 | ## Create the body content 187 | makeBody() 188 | 189 | ## Initialize the readouts 190 | global _readouts 191 | _readouts = document.querySelectorAll('.readout') 192 | for el in _readouts: 193 | el.style.fontSize = '12' 194 | 195 | 196 | ## Bind event handler to step change form 197 | ssform = document.getElementById('setstep') 198 | ssform.addEventListener('submit', handle_stepchange) 199 | 200 | ## Bind custom event handler to document 201 | document.addEventListener('state:update', update_readouts) 202 | 203 | ## define polling function 204 | global _state, _prior_state 205 | def update (): 206 | getState() 207 | ## Reload if server has restarted 208 | if (_prior_state is not None and 209 | _prior_state.hasOwnProperty('server_start_time')): 210 | if _state['server_start_time'] > _prior_state['server_start_time']: 211 | location.reload(True) 212 | 213 | ## First update 214 | update () 215 | ## Repeat every 0.5 secondss 216 | window.setInterval (update, 500) 217 | 218 | try: 219 | document.addEventListener('DOMContentLoaded', start) 220 | except NameError: 221 | pass 222 | 223 | 224 | 225 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Description: Server for NearlyPurePythonWebAppDemo. 5 | 6 | Sources and more info at: 7 | https://github.com/Michael-F-Ellis/NearlyPurePythonWebAppDemo 8 | 9 | Author: Mike Ellis 10 | Copyright 2017 Ellis & Grant, Inc 11 | License: MIT License 12 | """ 13 | 14 | 15 | ############################################################ 16 | # Initialization 17 | ############################################################ 18 | import os 19 | import sys 20 | import time 21 | import doctest 22 | import random 23 | import subprocess 24 | import bottle 25 | import common 26 | from traceback import format_exc 27 | from htmltree.htmltree import * 28 | 29 | ## We import client.py so the Bottle reloader will track it for changes 30 | ## It has no use in this module and no side effects other than a small 31 | ## overhead to load it. 32 | import client 33 | 34 | # Create an app instance. 35 | app = bottle.Bottle() 36 | request = bottle.request ## the request object accessor 37 | 38 | 39 | ############################################################ 40 | # Build index.html 41 | ############################################################ 42 | def buildIndexHtml(): 43 | """ 44 | Create the content index.html file. For the purposes of the demo, we 45 | create it with an empty body element to be filled in on the client side. 46 | Returns: None 47 | Raises: Nothing 48 | """ 49 | 50 | style = Style(**{'a:link':dict(color='red'), 51 | 'a:visited':dict(color='green'), 52 | 'a:hover':dict(color='hotpink'), 53 | 'a:active':dict(color='blue'), 54 | }) 55 | 56 | head = Head(style, Script(src='/client.js', charset='UTF-8')) 57 | 58 | body = Body("Replace me on the client side", 59 | style=dict(background_color='black')) 60 | 61 | doc = Html(head, body) 62 | return doc.render() 63 | 64 | 65 | ############################################################ 66 | # Routes and callback functions 67 | # The following routes are defined below: 68 | # /client.js 69 | # /home (= /index.html = /) 70 | # /getstate 71 | # /setstepsize 72 | ############################################################ 73 | 74 | @app.route('/client.js') 75 | def client(): 76 | """ 77 | Route for serving client.js 78 | """ 79 | root = os.path.abspath("./__javascript__") 80 | return bottle.static_file('client.js', root=root) 81 | 82 | @app.route("/") 83 | @app.route("/index.html") 84 | @app.route("/home") 85 | def index(): 86 | """ Serve the home page """ 87 | root = os.path.abspath("./__html__") 88 | return bottle.static_file('index.html', root=root) 89 | 90 | ## Module level variable used to exchange data beteen handlers 91 | _state = {} 92 | 93 | def stategen(): 94 | """ 95 | Initialize each state item with a random float between 0 and 10, then 96 | on each next() call, 'walk' the value by a randomly chosen increment. The 97 | purpose is to simulate a set of drifting measurements to be displayed 98 | and color coded on the client side. 99 | """ 100 | last = time.time() 101 | counter = 0 102 | nitems = common.nitems 103 | statekeys = common.statekeys 104 | _state['step'] = (-common.stepsize, 0.0, common.stepsize) 105 | _state['stepsize'] = common.stepsize 106 | statevalues = [round(random.random()*10, 2) for n in range(nitems)] 107 | _state.update(dict(zip(statekeys, statevalues))) 108 | while True: 109 | ## Update no more frequently than twice per second 110 | now = time.time() 111 | if now - last >= 0.5: 112 | last = now 113 | counter += 1 114 | step = _state['step'] 115 | statevalues = [round(v + random.choice(step), 2) for v in statevalues] 116 | statevalues = [min(10.0, max(0.0, v)) for v in statevalues] 117 | _state.update(dict(zip(statekeys, statevalues))) 118 | _state['count'] = counter 119 | yield 120 | 121 | ## The generator needs to persist outside of handlers. 122 | _stateg = stategen() 123 | 124 | @app.route("/getstate") 125 | def getstate(): 126 | """ 127 | Serve a JSON object representing state values. 128 | Returns: dict(count=n, item0=v0, item1=v1, ...) 129 | Raises: Nothing 130 | """ 131 | next(_stateg) 132 | return _state 133 | 134 | @app.post("/setstepsize") 135 | def setStepSize(): 136 | """ 137 | Called when user submits step size input. Validation 138 | happens client side so we don't check it here. In a real 139 | app you'd want some server-side validation would help protect 140 | against exploits. 141 | """ 142 | stepsize = float(request.forms.get('stepsize')) 143 | _state['stepsize'] = stepsize 144 | _state['step'] = (-stepsize, 0, stepsize) 145 | return {} 146 | 147 | ######################################################## 148 | # Build functions 149 | ######################################################## 150 | 151 | def needsBuild(target, sources): 152 | """ 153 | Returns True if target doesn't exist or is older than any of the sources. 154 | Sources must be an iterable, e.g. list or tuple. 155 | """ 156 | return not os.path.exists(target) or any([(os.stat(target).st_mtime 157 | < os.stat(source).st_mtime) for source in sources]) 158 | def doBuild(): 159 | """ 160 | Build the html and js files, if needed. 161 | 162 | Note: In larger projects with more complex dependencies, you'll probably 163 | want to use make or scons to build the targets instead of the simple 164 | approach taken here. 165 | """ 166 | ## build the index.html file 167 | index_sources = ('server.py', 'htmltree/htmltree.py', 'common.py') 168 | target = '__html__/index.html' 169 | if needsBuild(target, index_sources): 170 | os.makedirs(os.path.dirname(target), exist_ok=True) 171 | with open(target, 'w') as f: 172 | print(buildIndexHtml(),file=f) 173 | 174 | ## build the client.js file 175 | client_sources = ('client.py', 'htmltree/htmltree.py', 'common.py') 176 | if needsBuild('__javascript__/client.js', client_sources): 177 | proc = subprocess.Popen('transcrypt -b -n -m client.py', shell=True) 178 | if proc.wait() != 0: 179 | raise Exception("Failed trying to build client.js") 180 | 181 | class AppWrapperMiddleware: 182 | """ 183 | Some hosted environments, e.g. pythonanywhere.com, require you 184 | to export the Bottle app object. Exporting an instance of this 185 | wrapper class makes sure the build procedure runs on startup. 186 | """ 187 | def __init__(self, app): 188 | self.app = app 189 | def __call__(self, e, h): 190 | doBuild() 191 | return self.app(e,h) 192 | 193 | ################################################## 194 | ## Import this from external wsgi file 195 | app_for_wsgi_env = AppWrapperMiddleware(app) 196 | ################################################## 197 | 198 | ######################################################## 199 | ## Default wrapper so we can spawn this app commandline or 200 | ## from multiprocessing. 201 | ######################################################## 202 | def serve(server='wsgiref', port=8800, reloader=False, debugmode=False): 203 | """ 204 | Build the html and js files, if needed, then launch the app. 205 | 206 | The default server is the single-threaded 'wsgiref' server that comes with 207 | Python. It's fine for a demo, but for production you'll want to use 208 | something better, e.g. server='cherrypy'. For an extensive list of server 209 | options, see http://bottlepy.org/docs/dev/deployment.html 210 | """ 211 | bottle.debug(debugmode) 212 | 213 | ## Client side tracks _state['server_start_time'] 214 | ## to decide if it should reload. 215 | _state['server_start_time'] = time.time() 216 | 217 | ## rebuild as needed 218 | doBuild() 219 | 220 | ## Launch the web service loop. 221 | bottle.run(app, 222 | host='0.0.0.0', 223 | server=server, 224 | port=port, 225 | reloader=reloader, 226 | debug=debugmode) 227 | 228 | ################################################### 229 | ## The following runs only when we start from 230 | ## the command line. 231 | ################################################### 232 | if __name__ == '__main__': 233 | doctest.testmod() 234 | import argparse 235 | parser = argparse.ArgumentParser( 236 | description = "Nearly Pure Python Web App Demo") 237 | parser.add_argument('-s', '--server', type=str, default='wsgiref', 238 | help="server program to use.") 239 | parser.add_argument('-p', '--port', type=int, default=8800, 240 | help="port number to serve on.") 241 | parser.add_argument('--no-reloader', dest='reloader', action='store_false', 242 | help="disable reloader (defult: enabled)") 243 | parser.add_argument('--no-debug', dest='debug', action='store_false', 244 | help="disable debug mode (defult: enabled)") 245 | parser.set_defaults(reloader=True) 246 | parser.set_defaults(debug=True) 247 | args = parser.parse_args() 248 | serve(server=args.server, port=args.port, 249 | reloader=args.reloader, debugmode=args.debug) 250 | 251 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nearly Pure Python Web App Demo 2 | 3 | ## A simple but complete web application skeleton that runs 'out of the box'. 4 | 5 | * No static html, css, or js files 6 | * Demonstrates how to generate and manipulate html in pure python on both the client and server sides. 7 | * A useful skeleton that includes Ajax JSON state updates from server to client and vice versa. 8 | * Automatically reloads server and client pages when any source file changes. 9 | * Calls a few JS methods directly from Python, hence the 'nearly pure' in the title 10 | * Powered by Jacques De Hooge's [*Transcrypt™*](https://transcrypt.org/) Python to JS transpiler and Marcel Hellkamp's [*Bottle Python Web Framework.*](http://bottlepy.org/docs/dev/) 11 | 12 | ### Who's this for? 13 | * Developers with a taste for minimalism who want to experiment with a (mostly) pure python approach to web app development. 14 | * Course instructors looking a complete example students can use as a starting point. 15 | 16 | ### What can I do with it? 17 | * Fork or clone it as a starting point for your own projects. 18 | * Read the code. There are only 4 short files with less than 400 sloc total. 19 | * Stare at the colorful numbers until you grok the message hidden there *just for you*[1](#hint). 20 | 21 | ### Dependencies (install these first) 22 | * [Python]( https://www.python.org/downloads/) >= 3.5 23 | * [Transcrypt™](http://transcrypt.org/) >= 3.6.24 24 | * `pip install transcrypt` 25 | * [Bottle](http://bottlepy.org/docs/dev/) >= 0.12.13 26 | * `pip install bottle` 27 | * [htmltree](https://github.com/Michael-F-Ellis/htmltree) >= 0.7.5 28 | * pip install htmltree 29 | 30 | ### NEW Single Source Files 31 | Use the recently added `allinone.py` which combines the content of 3 files into a single one that automatically builds the Javascript and launches the server. Just do `python allinone.py` instead of `python server.py`. There are also two other new files, `minimal_allinone.py` and `serverless.py`. These files will likely be the focus of future development and, hence, will continue to diverge from the behavior of `server.py + client.py + common.py` which should now be considered deprecated, or at least discouraged. 32 | 33 | ### Installation and usage 34 | ``` 35 | git clone https://github.com/Michael-F-Ellis/NearlyPurePythonWebAppDemo 36 | cd NearlyPurePythonWebAppDemo 37 | python allinone.py 38 | ``` 39 | * Note: You can choose a different server and port. Do `python allinone.py -h` for details 40 | 41 | * browse to http://localhost:8800 . Your should see a screen like the one below with readout values updating every half-second. Values are color coded as follows: 42 | * blue: V <= 2.0 : blue 43 | * green: 2.0 < V < 8.0 : green 44 | * red: V >= 8.0 red 45 | 46 | * Use the slider to change the Step Size to any number between 0 and 10. 47 | * Larger values cause faster drifts through color ranges. 48 | 49 | ![Figure 1.](doc/img/nppwad2.gif) 50 | 51 | ### Rapid development 52 | * While the app is running, saving a change to any source file triggers a rebuild and reload of the server and the client page. See [Auto Reload](doc/AutoReload.md) for details. 53 | * Clean pythonic syntax for generating html. See [htmltree](https://github.com/Michael-F-Ellis/htmltree) docs for details. Here's the function that creates all the body html in the demo above. 54 | ``` 55 | def makeBody(): 56 | """ 57 | Create HTML for the body element content. This is done as a demo to show 58 | that the code in htmltree.py works in Transcrypted JS as well as in Python. 59 | It could have been accomplished just as easily on the server side. 60 | 61 | Uses JS: .innerHTML 62 | """ 63 | banner = H1("Nearly Pure Python Web App Demo", style=dict(color='yellow')) 64 | projectlink = A('Source Code on GitHub', 65 | href='https://github.com/Michael-F-Ellis/NearlyPurePythonWebAppDemo') 66 | subbanner = H2(projectlink) 67 | 68 | header = Div(banner, subbanner, style=dict(text_align='center')) 69 | 70 | ## Each readout is a div containing a meter element and a span to hold 71 | ## a text representaton of the current value. 72 | readouts = [] 73 | for datakey in common.statekeys: 74 | meter = Meter(min="0.1", low="2.0", high="8.0", max="10.0", 75 | style=dict(width="25%", margin_top="5px", margin_bottom="5px")) 76 | value = Span() 77 | readouts.append(Div(meter, value, _class='readout', data_key=datakey)) 78 | 79 | 80 | ## The step input is a range slider input with a label on the left and 81 | ## a span for the current value on the right. 82 | slider = Input(id='stepinput', _type='range', 83 | min="0.1", max="10.0", step="0.1", 84 | style=dict(margin='1em')) 85 | 86 | stepinput = Label("Step Size", slider, 87 | style=dict(color='white')) 88 | 89 | ## Make a div container for the step input. 90 | stepdiv = Div(stepinput, 91 | Span(id='stepvalue', style=dict(color="white")), 92 | style=dict(margin='20px')) 93 | 94 | ## Assemble header, readouts, and stepdiv within a div 95 | bodycontent = Div(header) 96 | bodycontent.C.extend(readouts) 97 | bodycontent.C.append(stepdiv) 98 | 99 | ## Use the DOM API to insert rendered content 100 | print(bodycontent.render(0)) 101 | document.body.innerHTML = bodycontent.render() 102 | ``` 103 | and here is the output `makeBody()` produces: 104 | ``` 105 |
106 |
107 |

108 | Nearly Pure Python Web App Demo 109 |

110 |

111 | 112 | Source Code on GitHub 113 | 114 |

115 |
116 |
117 | 118 | 119 | 120 | 121 |
122 |
123 | 124 | 125 | 126 | 127 |
128 |
129 | 130 | 131 | 132 | 133 |
134 |
135 | 136 | 137 | 138 | 139 |
140 |
141 | 142 | 143 | 144 | 145 |
146 |
147 | 148 | 149 | 150 | 151 |
152 |
153 | 154 | 155 | 156 | 157 |
158 |
159 | 160 | 161 | 162 | 163 |
164 |
165 | 166 | 167 | 168 | 169 |
170 |
171 | 172 | 173 | 174 | 175 |
176 |
177 | 181 | 182 | 183 |
184 |
185 | ``` 186 | ## Starting Smaller 187 | If `allinone.py` is at least somewhat close to where you want to start with development, you can simply make a copy under a new names and start hacking, but you might find a better starting point for your development in one of the two additional skeleton files provided in the repo. 188 | 189 | ### minimal_allinone.py 190 | This skeleton omits most of the content from allinone.py but retains the server instance, command line options and automatic reloading. The client-server state exchange is also kept but in very minimal form. The display is simply a headline with a counter updating once per second with a count supplied from the server. 191 | 192 | ![Figure 2.](doc/img/minimal.gif) 193 | 194 | 195 | ### serverless.py 196 | As the name implies, this skeleton has the server code removed. It's a once-through script that generates an index.html file and a js file with Transcrypt. The script finishes by using Python's built-in web browser module to open the index file as a `file://` URL in your default web browser. When the index file loads it fetches and runs the JS. The display is identical in form to the one from `minimal_allinone.py` but the counter updating is handled locally in JS. 197 | 198 | ![Figure 3.](doc/img/serverless.gif) 199 | 200 | ### Files 201 | Here's what comes from the repository: 202 | ``` 203 | ├── License.md 204 | ├── README.md -- This document 205 | ├── allinone.py -- Main script 206 | ├── client.py (deprecated) 207 | ├── common.py (deprecated) 208 | ├── doc 209 | │   ├── AutoReload.md 210 | │   ├── example_wsgi.py 211 | │   └── img 212 | │   └── nppwad.gif 213 | ├── minimal_allinone.py.py 214 | ├── server.py (deprecated) 215 | ├── serverless.py 216 | 217 | ``` 218 | Ater running `allinone.py` for the first time, files are generated and the directory tree will look like: 219 | 220 | ``` 221 | ├── License.md 222 | ├── README.md 223 | ├── __html__ 224 | │   └── index.html 225 | ├── __javascript__ 226 | │   ├── allinone.js 227 | │   ├── allinone.mod.js 228 | │   └── extra 229 | │   └── sourcemap 230 | │   ├── allinone.js.map 231 | │   └── allinone.mod.js.map 232 | ├── allinone.py 233 | ├── client.py 234 | ├── common.py 235 | ├── doc 236 | │   ├── AutoReload.md 237 | │   ├── example_wsgi.py 238 | │   └── img 239 | │   └── nppwad.gif 240 | ├── minimal_allinone.py.py 241 | ├── server.py 242 | ├── serverless.py 243 | 244 | ``` 245 | 246 | ## Parting Thoughts 247 | ### The Bad News 248 | While this approach to development can save you from the frustrations of dealing with .html, .css, and .js syntax, it can't save you from the need to *understand* the Document Object Model, browser events, ajax, http request routing, etc. 249 | 250 | ### The Good News 251 | If you're already comfortable in Python and understand what goes on in a browser and web server, you can use these skeletons as a starting point for developing entirely in Python. 252 |
253 | Footnotes 254 | 255 | 1: Neuro-chemical assistance may be required. ;-) 256 | -------------------------------------------------------------------------------- /minimal_allinone.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Combines contents of common.py, client.py, server.py into a single 4 | module. There are 3 sections: 5 | 6 | Common Code -- used by both client and server 7 | Server Code -- Specific to the web service 8 | Client Code -- Compiled to JS by Transcrypt 9 | 10 | You can search for the above names in your editor for convenient navigation. 11 | 12 | External dependencies: 13 | Python >= 3.5 (Available from www.python.org) 14 | pip install transcrypt 15 | pip install bottlepy 16 | pip install htmltree 17 | 18 | USAGE: 19 | Typically: 20 | $ python minimal_allinone.py 21 | 22 | Help is available with the -h option. 23 | 24 | $ python minimal_allinone.py -h 25 | usage: minimal_allinone.py [-h] [-s SERVER] [-p PORT] [--no-reloader] [--no-debug] 26 | 27 | Nearly Pure Python Web App Demo 28 | 29 | optional arguments: 30 | -h, --help show this help message and exit 31 | -s SERVER, --server SERVER 32 | server program to use. 33 | -p PORT, --port PORT port number to serve on (default: 8800). 34 | --no-reloader disable reloader (defult: enabled) 35 | --no-debug disable debug mode (defult: enabled) 36 | 37 | Author: Mike Ellis 38 | Copyright 2017 Ellis & Grant, Inc 39 | License: MIT License 40 | 41 | This file is part of NearlyPurePythonWebAppDemo 42 | https://github.com/Michael-F-Ellis/NearlyPurePythonWebAppDemo 43 | """ 44 | ####################################################################### 45 | ## Common Code 46 | ## Used by both client and server 47 | ####################################################################### 48 | from htmltree.htmltree import * 49 | 50 | ## Module globals ## 51 | _state = {} 52 | ## If working from a renamed copy of this module, change the folowing 53 | ## line accordingly. 54 | _module_basename = 'minimal_allinone' 55 | _module_pyname = _module_basename + '.py' 56 | _module_jsname = _module_basename + '.js' 57 | 58 | ## A little bit of trickiness here. We need to cause CPython to see only 59 | ## what's intended to be server-side code and Transcrypt to see only what's 60 | ## intended be client-side code. The solution is to try to invoke the Transcrypt 61 | ## pragma function. It won't be defined in CPython, so the server code can 62 | ## go in the 'except' branch (wrapped in skip/noskip pragmas in comments) 63 | ## and the client code follows in an 'else' branch. 64 | try: 65 | __pragma__('js', "") 66 | except NameError: 67 | ####################################################################### 68 | ## Server Code 69 | ####################################################################### 70 | #__pragma__('skip') 71 | 72 | # Initialization 73 | import os 74 | import sys 75 | import time 76 | import random 77 | import subprocess 78 | import bottle 79 | from traceback import format_exc 80 | 81 | # Create an app instance. 82 | app = bottle.Bottle() 83 | request = bottle.request ## the request object accessor 84 | 85 | 86 | # Routes and callback functions 87 | # The following routes are defined below: 88 | # /client.js 89 | # /home (= /index.html = /) 90 | # /getstate 91 | # /setstepsize 92 | 93 | @app.route('/' + _module_jsname) 94 | def client(): 95 | """ 96 | Route for serving client js 97 | """ 98 | root = os.path.abspath("./__javascript__") 99 | return bottle.static_file(_module_jsname, root=root) 100 | 101 | @app.route("/") 102 | @app.route("/index.html") 103 | @app.route("/home") 104 | def index(): 105 | """ Serve the home page """ 106 | root = os.path.abspath("./__html__") 107 | return bottle.static_file('index.html', root=root) 108 | 109 | def stategen(): 110 | """ 111 | In a real app, this generator would update the state at each invocation 112 | for transmission to clients. Here we're just incrmenting a counter. 113 | """ 114 | counter = 0 115 | while True: 116 | counter += 1 117 | _state['count'] = counter 118 | yield 119 | 120 | ## The generator instance needs to persist outside of handlers. 121 | _stateg = stategen() 122 | 123 | @app.route("/getstate") 124 | def getstate(): 125 | """ 126 | Serve a JSON object representing state values. 127 | Returns: dict() 128 | Raises: Nothing 129 | """ 130 | next(_stateg) 131 | return _state 132 | 133 | 134 | ## Build functions 135 | 136 | def needsBuild(target, sources): 137 | """ 138 | Returns True if target doesn't exist or is older than any of the sources. 139 | Sources must be an iterable, e.g. list or tuple. 140 | """ 141 | return not os.path.exists(target) or any([(os.stat(target).st_mtime 142 | < os.stat(source).st_mtime) for source in sources]) 143 | def doBuild(): 144 | """ 145 | Build the html and js files, if needed. 146 | 147 | Note: In larger projects with more complex dependencies, you'll probably 148 | want to use make or scons to build the targets instead of the simple 149 | approach taken here. 150 | """ 151 | ## build the index.html file 152 | index_sources = (_module_pyname,) 153 | target = '__html__/index.html' 154 | if needsBuild(target, index_sources): 155 | os.makedirs(os.path.dirname(target), exist_ok=True) 156 | with open(target, 'w') as f: 157 | print(buildIndexHtml(),file=f) 158 | 159 | ## build the client.js file 160 | client_sources = (_module_pyname,) 161 | if needsBuild('__javascript__/' + _module_jsname, client_sources): 162 | proc = subprocess.Popen('transcrypt -b -n -m {}'.format(_module_pyname), shell=True) 163 | if proc.wait() != 0: 164 | raise Exception("Failed trying to build {}".format(_module_jsname)) 165 | 166 | def buildIndexHtml(): 167 | """ 168 | Create the content index.html file. For the purposes of the demo, we 169 | create it with an empty body element to be filled in on the client side. 170 | """ 171 | viewport = Meta(name='viewport', content='width=device-width, initial-scale=1') 172 | 173 | head = Head(viewport, 174 | Script(src='/' + _module_jsname, 175 | charset='UTF-8')) 176 | 177 | body = Body() 178 | 179 | doc = Html(head, body) 180 | return doc.render() 181 | 182 | ## App service classes and functions 183 | class AppWrapperMiddleware: 184 | """ 185 | Some hosted environments, e.g. pythonanywhere.com, require you 186 | to export the Bottle app object. Exporting an instance of this 187 | wrapper class makes sure the build procedure runs on startup. 188 | """ 189 | def __init__(self, app): 190 | self.app = app 191 | def __call__(self, e, h): 192 | doBuild() 193 | return self.app(e,h) 194 | 195 | ## Import this from external wsgi file 196 | app_for_wsgi_env = AppWrapperMiddleware(app) 197 | 198 | ## Default wrapper so we can spawn this app from commandline or 199 | ## from multiprocessing. 200 | def serve(server='wsgiref', port=8800, reloader=False, debugmode=False): 201 | """ 202 | Build the html and js files, if needed, then launch the app. 203 | 204 | The default server is the single-threaded 'wsgiref' server that comes with 205 | Python. It's fine for a demo, but for production you'll want to use 206 | something better, e.g. server='cherrypy'. For an extensive list of server 207 | options, see http://bottlepy.org/docs/dev/deployment.html 208 | """ 209 | bottle.debug(debugmode) 210 | 211 | ## Client side tracks _state['server_start_time'] 212 | ## to decide if it should reload. 213 | _state['server_start_time'] = time.time() 214 | 215 | ## rebuild as needed 216 | doBuild() 217 | 218 | ## Launch the web service loop. 219 | bottle.run(app, 220 | host='0.0.0.0', 221 | server=server, 222 | port=port, 223 | reloader=reloader, 224 | debug=debugmode) 225 | 226 | ## The following runs only when we start from 227 | ## the command line. 228 | if __name__ == '__main__': 229 | import argparse 230 | parser = argparse.ArgumentParser( 231 | description = "My Web App") 232 | parser.add_argument('-s', '--server', type=str, default='wsgiref', 233 | help="server program to use.") 234 | parser.add_argument('-p', '--port', type=int, default=8800, 235 | help="port number to serve on. (8800)") 236 | parser.add_argument('--no-reloader', dest='reloader', action='store_false', 237 | help="disable reloader (defult: enabled)") 238 | parser.add_argument('--no-debug', dest='debug', action='store_false', 239 | help="disable debug mode (defult: enabled)") 240 | parser.set_defaults(reloader=True) 241 | parser.set_defaults(debug=True) 242 | args = parser.parse_args() 243 | serve(server=args.server, port=args.port, 244 | reloader=args.reloader, debugmode=args.debug) 245 | 246 | #__pragma__('noskip') 247 | ## ------------- End of Server Code ----------------------------------- 248 | 249 | pass ## needed so the except branch looks complete to Transcrypt. 250 | else: 251 | 252 | ####################################################################### 253 | ## Client Code 254 | ## Compiled by Transcrypt 255 | ####################################################################### 256 | 257 | _prior_state = {} 258 | _readouts = None 259 | 260 | def makeBody(): 261 | """ 262 | Create HTML for the body element content. This is done as a demo to show 263 | that the code in htmltree.py works in Transcrypted JS as well as in Python. 264 | It could have been accomplished just as easily on the server side. 265 | 266 | Uses JS: .innerHTML, .style 267 | """ 268 | document.body.style.backgroundColor = "black" 269 | banner = H1("Hello, {}.".format(_module_basename), style=dict(color='yellow')) 270 | counter = H2(id="counter", style=dict(color='green')) 271 | header = Div(banner, counter, style=dict(text_align='center')) 272 | bodycontent = Div(header) 273 | 274 | ## Use the DOM API to insert rendered content 275 | document.body.innerHTML = bodycontent.render() 276 | 277 | # jQuery replacement functions 278 | # The next 3 functions provide some replacements for frequently used 279 | # jQquery methods. They could logically be placed in a separate module. 280 | def triggerCustomEvent(name, data): 281 | """ 282 | JS version of jQuery.trigger. 283 | see http://youmightnotneedjquery.com/#trigger_custom 284 | 285 | Uses JS: CustomEvent, .createEvent, .dispatchEvent 286 | """ 287 | if window.CustomEvent: 288 | event = __new__ (CustomEvent(name, {'detail' : data})) 289 | else: 290 | event = document.createEvent('CustomEvent') 291 | event.initCustomEvent(name, True, True, data) 292 | document.dispatchEvent(event) 293 | 294 | def getJSON(url, f): 295 | """ 296 | JS version of jQuery.getJSON 297 | see http://youmightnotneedjquery.com/#get_json 298 | url must return a JSON string 299 | f(data) handles an object parsed from the return JSON string 300 | 301 | Uses JS: XMLHttpRequest, JSON.parse 302 | """ 303 | request = __new__ (XMLHttpRequest()) 304 | request.open('GET', url, True) 305 | def onload(): 306 | if 200 <= request.status < 400: 307 | data = JSON.parse(request.responseText) 308 | f(data) ## call handler with object created from JSON string 309 | else: 310 | _ = "Server returned {} for getJSON request on {}".format(request.status, url) 311 | console.log(_) 312 | def onerror(): 313 | _ = "Connection error for getJSON request on {}".format(url) 314 | console.log(_) 315 | request.onload = onload 316 | request.onerror = onerror 317 | request.send() 318 | 319 | def post(url, data): 320 | """ 321 | JS version of jQuery.post 322 | see http://youmightnotneedjquery.com/#post 323 | data is expected to be a dict. 324 | 325 | Uses JS: XMLHttpRequest, .hasOwnProperty, encodeURIComponent 326 | """ 327 | request = __new__(XMLHttpRequest()) 328 | request.open('POST', url, True) 329 | request.setRequestHeader('Content-Type', 330 | 'application/x-www-form-urlencoded; ' 331 | 'charset=UTF-8') 332 | ## serialize the data, see http://stackoverflow.com/a/1714899/426853 333 | ldata = [] 334 | for k,v in data.items(): 335 | if data.hasOwnProperty(k): 336 | lh = encodeURIComponent(k) 337 | rh = encodeURIComponent(v) 338 | ldata.append("{}={}".format(lh, rh)) 339 | 340 | request.send("&".join(ldata)) 341 | 342 | ## End of jQuery replacement functions ## 343 | 344 | ## Application callbacks ## 345 | def getState(): 346 | """ Fetch JSON obj containing monitored variables. """ 347 | def f(data): 348 | global _state, _prior_state 349 | _prior_state.update(_state) 350 | _state = data 351 | triggerCustomEvent('state:update', {}) 352 | #console.log(_state) 353 | getJSON('/getstate', f) 354 | return 355 | 356 | 357 | def start (): 358 | """ 359 | Client-side app execution starts here. 360 | 361 | Uses JS: .getElementById, .addEventListener, 362 | .hasOwnProperty, location, .setInterval 363 | """ 364 | ## Create the body content 365 | makeBody() 366 | 367 | ## Bind event handlers ## 368 | counter = document.getElementById('counter') 369 | 370 | def update_counter(e): 371 | counter.textContent = "{}".format(_state['count']) 372 | 373 | document.addEventListener('state:update', update_counter) 374 | 375 | ## define polling function 376 | global _state, _prior_state 377 | def update (): 378 | getState() 379 | ## Reload if server has restarted 380 | if (_prior_state is not None and 381 | _prior_state.hasOwnProperty('server_start_time')): 382 | if _state['server_start_time'] > _prior_state['server_start_time']: 383 | location.reload(True) 384 | 385 | ## First update 386 | update () 387 | ## Repeat every second. 388 | window.setInterval (update, 1000) 389 | 390 | ## Wait until the DOM is loaded before calling start() 391 | document.addEventListener('DOMContentLoaded', start) 392 | 393 | ## ------------- End of Client Code ----------------------------------- 394 | -------------------------------------------------------------------------------- /allinone.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Combines contents of common.py, client.py, server.py into a single 4 | module. There are 3 sections: 5 | 6 | Common Code -- used by both client and server 7 | Server Code -- Specific to the web service 8 | Client Code -- Compiled to JS by Transcrypt 9 | 10 | You can search for the above names in your editor for convenient navigation. 11 | 12 | External dependencies: 13 | Python >= 3.5 (Available from www.python.org) 14 | pip install transcrypt 15 | pip install bottlepy 16 | pip install htmltree 17 | 18 | USAGE: 19 | Typically: 20 | $ python allinone.py 21 | 22 | Help is available with the -h option. 23 | 24 | $ python allinone.py -h 25 | usage: allinone.py [-h] [-s SERVER] [-p PORT] [--no-reloader] [--no-debug] 26 | 27 | Nearly Pure Python Web App Demo 28 | 29 | optional arguments: 30 | -h, --help show this help message and exit 31 | -s SERVER, --server SERVER 32 | server program to use. 33 | -p PORT, --port PORT port number to serve on (default: 8800). 34 | --no-reloader disable reloader (defult: enabled) 35 | --no-debug disable debug mode (defult: enabled) 36 | 37 | Author: Mike Ellis 38 | Copyright 2017 Ellis & Grant, Inc 39 | License: MIT License 40 | 41 | This file is part of NearlyPurePythonWebAppDemo 42 | https://github.com/Michael-F-Ellis/NearlyPurePythonWebAppDemo 43 | """ 44 | ####################################################################### 45 | ## Common Code 46 | ## Used by both client and server 47 | ####################################################################### 48 | from htmltree.htmltree import * 49 | class Common(): 50 | def __init__(self, nitems, stepsize): 51 | ## The number of state items to display in the web page. 52 | self.nitems = nitems 53 | ## Step size for the simulation 54 | self.stepsize = stepsize 55 | ## Id strings for readouts 56 | self.statekeys = ["item{}".format(n) for n in range(nitems)] 57 | 58 | common = Common(10, 0.5) 59 | 60 | ## A little bit of trickiness here. We need to cause CPython to see only 61 | ## what's intended to be server-side code and Transcrypt to see only what's 62 | ## intended be client-side code. The solution is to try to invoke the Transcrypt 63 | ## pragma function. It won't be defined in CPython, so the server code can 64 | ## go in the 'except' branch (wrapped in skip/noskip pragmas in comments) 65 | ## and the client code follows in an 'else' branch. 66 | try: 67 | __pragma__('js', "") 68 | except NameError: 69 | ####################################################################### 70 | ## Server Code 71 | ####################################################################### 72 | #__pragma__('skip') 73 | 74 | # Initialization 75 | import os 76 | import sys 77 | import time 78 | import random 79 | import subprocess 80 | import bottle 81 | from traceback import format_exc 82 | 83 | # Create an app instance. 84 | app = bottle.Bottle() 85 | request = bottle.request ## the request object accessor 86 | 87 | 88 | # Routes and callback functions 89 | # The following routes are defined below: 90 | # /client.js 91 | # /home (= /index.html = /) 92 | # /getstate 93 | # /setstepsize 94 | 95 | @app.route('/allinone.js') 96 | def client(): 97 | """ 98 | Route for serving client.js 99 | """ 100 | root = os.path.abspath("./__javascript__") 101 | return bottle.static_file('allinone.js', root=root) 102 | 103 | @app.route("/") 104 | @app.route("/index.html") 105 | @app.route("/home") 106 | def index(): 107 | """ Serve the home page """ 108 | root = os.path.abspath("./__html__") 109 | return bottle.static_file('index.html', root=root) 110 | 111 | ## Module level variable used to exchange data beteen handlers 112 | _state = {} 113 | 114 | def stategen(): 115 | """ 116 | Initialize each state item with a random float between 0 and 10, then 117 | on each next() call, 'walk' the value by a randomly chosen increment. The 118 | purpose is to simulate a set of drifting measurements to be displayed 119 | and color coded on the client side. 120 | """ 121 | last = time.time() 122 | counter = 0 123 | nitems = common.nitems 124 | statekeys = common.statekeys 125 | _state['step'] = (-common.stepsize, 0.0, common.stepsize) 126 | _state['stepsize'] = common.stepsize 127 | statevalues = [round(random.random()*10, 2) for n in range(nitems)] 128 | _state.update(dict(zip(statekeys, statevalues))) 129 | while True: 130 | ## Update no more frequently than twice per second 131 | now = time.time() 132 | if now - last >= 0.5: 133 | last = now 134 | counter += 1 135 | step = _state['step'] 136 | statevalues = [round(v + random.choice(step), 2) for v in statevalues] 137 | statevalues = [min(10.0, max(0.0, v)) for v in statevalues] 138 | _state.update(dict(zip(statekeys, statevalues))) 139 | _state['count'] = counter 140 | yield 141 | 142 | ## The generator needs to persist outside of handlers. 143 | _stateg = stategen() 144 | 145 | @app.route("/getstate") 146 | def getstate(): 147 | """ 148 | Serve a JSON object representing state values. 149 | Returns: dict(count=n, item0=v0, item1=v1, ...) 150 | Raises: Nothing 151 | """ 152 | next(_stateg) 153 | return _state 154 | 155 | @app.post("/setstepsize") 156 | def setStepSize(): 157 | """ 158 | Called when user submits step size input. Validation 159 | happens client side so we don't check it here. In a real 160 | app you'd want some server-side validation would help protect 161 | against exploits. 162 | """ 163 | stepsize = float(request.forms.get('stepsize')) 164 | _state['stepsize'] = stepsize 165 | _state['step'] = (-stepsize, 0, stepsize) 166 | return {} 167 | 168 | # Build functions 169 | 170 | def needsBuild(target, sources): 171 | """ 172 | Returns True if target doesn't exist or is older than any of the sources. 173 | Sources must be an iterable, e.g. list or tuple. 174 | """ 175 | return not os.path.exists(target) or any([(os.stat(target).st_mtime 176 | < os.stat(source).st_mtime) for source in sources]) 177 | def doBuild(): 178 | """ 179 | Build the html and js files, if needed. 180 | 181 | Note: In larger projects with more complex dependencies, you'll probably 182 | want to use make or scons to build the targets instead of the simple 183 | approach taken here. 184 | """ 185 | ## build the index.html file 186 | index_sources = ('allinone.py',) 187 | target = '__html__/index.html' 188 | if needsBuild(target, index_sources): 189 | os.makedirs(os.path.dirname(target), exist_ok=True) 190 | with open(target, 'w') as f: 191 | print(buildIndexHtml(),file=f) 192 | 193 | ## build the client.js file 194 | client_sources = ('allinone.py',) 195 | if needsBuild('__javascript__/allinone.js', client_sources): 196 | proc = subprocess.Popen('transcrypt -b -n -m allinone.py', shell=True) 197 | if proc.wait() != 0: 198 | raise Exception("Failed trying to build allinone.js") 199 | 200 | def buildIndexHtml(): 201 | """ 202 | Create the content index.html file. For the purposes of the demo, we 203 | create it with an empty body element to be filled in on the client side. 204 | """ 205 | viewport = Meta(name='viewport', content='width=device-width, initial_scale=1') 206 | 207 | style = Style(**{'a:link':dict(color='red'), 208 | 'a:visited':dict(color='green'), 209 | 'a:hover':dict(color='hotpink'), 210 | 'a:active':dict(color='blue'), 211 | }) 212 | 213 | head = Head(viewport, 214 | style, 215 | Script(src='/allinone.js', 216 | charset='UTF-8')) 217 | 218 | body = Body("Replace me on the client side", 219 | style=dict(background_color='black')) 220 | 221 | doc = Html(head, body) 222 | return doc.render() 223 | 224 | ## App service classes and functions 225 | class AppWrapperMiddleware: 226 | """ 227 | Some hosted environments, e.g. pythonanywhere.com, require you 228 | to export the Bottle app object. Exporting an instance of this 229 | wrapper class makes sure the build procedure runs on startup. 230 | """ 231 | def __init__(self, app): 232 | self.app = app 233 | def __call__(self, e, h): 234 | doBuild() 235 | return self.app(e,h) 236 | 237 | ## Import this from external wsgi file 238 | app_for_wsgi_env = AppWrapperMiddleware(app) 239 | 240 | ## Default wrapper so we can spawn this app from commandline or 241 | ## from multiprocessing. 242 | def serve(server='wsgiref', port=8800, reloader=False, debugmode=False): 243 | """ 244 | Build the html and js files, if needed, then launch the app. 245 | 246 | The default server is the single-threaded 'wsgiref' server that comes with 247 | Python. It's fine for a demo, but for production you'll want to use 248 | something better, e.g. server='cherrypy'. For an extensive list of server 249 | options, see http://bottlepy.org/docs/dev/deployment.html 250 | """ 251 | bottle.debug(debugmode) 252 | 253 | ## Client side tracks _state['server_start_time'] 254 | ## to decide if it should reload. 255 | _state['server_start_time'] = time.time() 256 | 257 | ## rebuild as needed 258 | doBuild() 259 | 260 | ## Launch the web service loop. 261 | bottle.run(app, 262 | host='0.0.0.0', 263 | server=server, 264 | port=port, 265 | reloader=reloader, 266 | debug=debugmode) 267 | 268 | ## The following runs only when we start from 269 | ## the command line. 270 | if __name__ == '__main__': 271 | import argparse 272 | parser = argparse.ArgumentParser( 273 | description = "Nearly Pure Python Web App Demo") 274 | parser.add_argument('-s', '--server', type=str, default='wsgiref', 275 | help="server program to use.") 276 | parser.add_argument('-p', '--port', type=int, default=8800, 277 | help="port number to serve on. (8800)") 278 | parser.add_argument('--no-reloader', dest='reloader', action='store_false', 279 | help="disable reloader (defult: enabled)") 280 | parser.add_argument('--no-debug', dest='debug', action='store_false', 281 | help="disable debug mode (defult: enabled)") 282 | parser.set_defaults(reloader=True) 283 | parser.set_defaults(debug=True) 284 | args = parser.parse_args() 285 | serve(server=args.server, port=args.port, 286 | reloader=args.reloader, debugmode=args.debug) 287 | 288 | #__pragma__('noskip') 289 | ## ------------- End of Server Code ----------------------------------- 290 | 291 | pass ## needed so the except branch looks complete to Transcrypt. 292 | else: 293 | 294 | ####################################################################### 295 | ## Client Code 296 | ## Compiled by Transcrypt as 'allinone.js' 297 | ####################################################################### 298 | 299 | _state = {} 300 | _prior_state = {} 301 | _readouts = None 302 | 303 | def makeBody(): 304 | """ 305 | Create HTML for the body element content. This is done as a demo to show 306 | that the code in htmltree.py works in Transcrypted JS as well as in Python. 307 | It could have been accomplished just as easily on the server side. 308 | 309 | Uses JS: .innerHTML 310 | """ 311 | banner = H1("Nearly Pure Python Web App Demo", style=dict(color='yellow')) 312 | projectlink = A('Source Code on GitHub', 313 | href='https://github.com/Michael-F-Ellis/NearlyPurePythonWebAppDemo') 314 | subbanner = H2(projectlink) 315 | 316 | header = Div(banner, subbanner, style=dict(text_align='center')) 317 | 318 | ## Each readout is a div containing a meter element and a span to hold 319 | ## a text representaton of the current value. 320 | readouts = [] 321 | for datakey in common.statekeys: 322 | meter = Meter(min="0.1", low="2.0", high="8.0", max="10.0", 323 | style=dict(width="25%", margin_top="5px", margin_bottom="5px")) 324 | value = Span() 325 | readouts.append(Div(meter, value, _class='readout', data_key=datakey)) 326 | 327 | 328 | ## The step input is a range slider input with a label on the left and 329 | ## a span for the current value on the right. 330 | slider = Input(id='stepinput', _type='range', 331 | min="0.1", max="10.0", step="0.1", 332 | style=dict(margin='1em')) 333 | 334 | stepinput = Label("Step Size", slider, 335 | style=dict(color='white')) 336 | 337 | ## Make a div container for the step input. 338 | stepdiv = Div(stepinput, 339 | Span(id='stepvalue', style=dict(color="white")), 340 | style=dict(margin='20px')) 341 | 342 | ## Assemble header, readouts, and stepdiv within a div 343 | bodycontent = Div(header) 344 | bodycontent.C.extend(readouts) 345 | bodycontent.C.append(stepdiv) 346 | 347 | ## Use the DOM API to insert rendered content 348 | print(bodycontent.render(0)) 349 | document.body.innerHTML = bodycontent.render() 350 | 351 | # jQuery replacement functions 352 | # The next 3 functions could logically be placed in a 353 | # separate module. 354 | def triggerCustomEvent(name, data): 355 | """ 356 | JS version of jQuery.trigger. 357 | see http://youmightnotneedjquery.com/#trigger_custom 358 | 359 | Uses JS: CustomEvent, .createEvent, .dispatchEvent 360 | """ 361 | if window.CustomEvent: 362 | event = __new__ (CustomEvent(name, {'detail' : data})) 363 | else: 364 | event = document.createEvent('CustomEvent') 365 | event.initCustomEvent(name, True, True, data) 366 | document.dispatchEvent(event) 367 | 368 | def getJSON(url, f): 369 | """ 370 | JS version of jQuery.getJSON 371 | see http://youmightnotneedjquery.com/#get_json 372 | url must return a JSON string 373 | f(data) handles an object parsed from the return JSON string 374 | 375 | Uses JS: XMLHttpRequest, JSON.parse 376 | """ 377 | request = __new__ (XMLHttpRequest()) 378 | request.open('GET', url, True) 379 | def onload(): 380 | if 200 <= request.status < 400: 381 | data = JSON.parse(request.responseText) 382 | f(data) ## call handler with object created from JSON string 383 | else: 384 | _ = "Server returned {} for getJSON request on {}".format(request.status, url) 385 | console.log(_) 386 | def onerror(): 387 | _ = "Connection error for getJSON request on {}".format(url) 388 | console.log(_) 389 | request.onload = onload 390 | request.onerror = onerror 391 | request.send() 392 | 393 | def post(url, data): 394 | """ 395 | JS version of jQuery.post 396 | see http://youmightnotneedjquery.com/#post 397 | data is expected to be a dict. 398 | 399 | Uses JS: XMLHttpRequest, .hasOwnProperty, encodeURIComponent 400 | """ 401 | request = __new__(XMLHttpRequest()) 402 | request.open('POST', url, True) 403 | request.setRequestHeader('Content-Type', 404 | 'application/x-www-form-urlencoded; ' 405 | 'charset=UTF-8') 406 | ## serialize the data, see http://stackoverflow.com/a/1714899/426853 407 | ldata = [] 408 | for k,v in data.items(): 409 | if data.hasOwnProperty(k): 410 | lh = encodeURIComponent(k) 411 | rh = encodeURIComponent(v) 412 | ldata.append("{}={}".format(lh, rh)) 413 | 414 | request.send("&".join(ldata)) 415 | 416 | # End of j!uery replacement functions 417 | 418 | ## Application callbacks 419 | def getState(): 420 | """ Fetch JSON obj containing monitored variables. """ 421 | def f(data): 422 | global _state, _prior_state 423 | _prior_state.update(_state) 424 | _state = data 425 | triggerCustomEvent('state:update', {}) 426 | #console.log(_state) 427 | getJSON('/getstate', f) 428 | return 429 | 430 | def update_readouts(): 431 | """ 432 | Triggered on each readout by 'state:update' custom event. We check each 433 | state value and alter it's text color accordingly. 434 | 435 | Uses JS: .getAttribute, .children, .textContent, .setAttribute, .getElementById, 436 | .activeElement 437 | """ 438 | ## queue the new values and colora 439 | queue = [] 440 | for el in _readouts: 441 | key = el.getAttribute('data-key') 442 | value = _state[key] 443 | valuef = float(value) 444 | if valuef <= 2.0: 445 | color = 'deepskyblue' 446 | elif valuef >= 8.0: 447 | color = 'red' 448 | else: 449 | color = 'green' 450 | queue.append((el, value, color)) 451 | 452 | ## write them to the DOM 453 | for el, value, color in queue: 454 | meter, span = el.children[0], el.children[1] 455 | span.textContent = value 456 | span.setAttribute('style', "padding-left:0.5em; color:{}; font-size:32;".format(color)) 457 | meter.value = value 458 | 459 | ## Also update the stepsize input with the current value, but 460 | ## check that the element does not have focus before doing so 461 | ## tp prevent update while user is typing. 462 | inp = document.getElementById('stepinput') 463 | readout = document.getElementById('stepvalue') 464 | if inp != document.activeElement: 465 | inp.value = _state['stepsize'] 466 | readout.style.color = "white" 467 | readout.innerHTML = _state['stepsize'] 468 | 469 | def handle_stepchange(event): 470 | """ 471 | Check that the request for a new step size is a number between 0 and 10 472 | before allowing the submit action to proceed. 473 | 474 | Uses JS: .getElementById, alert, parseFloat, isNaN 475 | """ 476 | fail_msg = "Step size must be a number between 0 and 10" 477 | v = document.getElementById('stepinput').value 478 | # Transcrypt float() is buggy, so use some inline JS. 479 | # See https://github.com/QQuick/Transcrypt/issues/314 480 | #__pragma__('js','{}','var vj = parseFloat(v); var isfloat = !isNaN(vj);') 481 | if isfloat and (0.0 <= vj <= 10.0): 482 | ## It's valid. Send it. 483 | post('/setstepsize', { 'stepsize': v }) 484 | return False 485 | else: 486 | alert(fail_msg) 487 | return False 488 | 489 | def handle_stepinput(event): 490 | """ Update the readout as the stepchange slider is dragged. """ 491 | readout = document.getElementById('stepvalue') 492 | v = document.getElementById('stepinput').value 493 | readout.style.color = "yellow" 494 | readout.innerHTML = v 495 | return False 496 | 497 | def start (): 498 | """ 499 | Client-side app execution starts here. 500 | 501 | Uses JS: .querySelectorAll, .style, .getElementById, .addEventListener, 502 | .hasOwnProperty, location, .setInterval 503 | """ 504 | ## Create the body content 505 | makeBody() 506 | 507 | ## Initialize the readouts 508 | global _readouts 509 | _readouts = document.querySelectorAll('.readout') 510 | for el in _readouts: 511 | el.style.fontSize = '12' 512 | 513 | 514 | ## Bind event handler to step change form 515 | ssinput = document.getElementById('stepinput') 516 | ssinput.addEventListener('change', handle_stepchange) 517 | ssinput.addEventListener('input', handle_stepinput) 518 | 519 | ## Bind custom event handler to document 520 | document.addEventListener('state:update', update_readouts) 521 | 522 | ## define polling function 523 | global _state, _prior_state 524 | def update (): 525 | getState() 526 | ## Reload if server has restarted 527 | if (_prior_state is not None and 528 | _prior_state.hasOwnProperty('server_start_time')): 529 | if _state['server_start_time'] > _prior_state['server_start_time']: 530 | location.reload(True) 531 | 532 | ## First update 533 | update () 534 | ## Repeat every 0.5 secondss 535 | window.setInterval (update, 500) 536 | 537 | ## Wait until the DOM is loaded before calling start() 538 | document.addEventListener('DOMContentLoaded', start) 539 | 540 | ## ------------- End of Client Code ----------------------------------- 541 | --------------------------------------------------------------------------------