- quickly serves static folders and launches webpage
- provides controlled and monitored front-end access to server-side Python objects via websockets
- sets up channels which websocket clients can subscribe to in order to receive updates about changes to back-end data without the need for polling
- provides some quickstart javascript/html tools to improve development speed, including custom-element templates
This module provides a concise and configurable syntax for launching a Tornado Web Application and initializing front-end Javascript Proxy objects which can get, set, call, or delete some or all attributes of a Python object.
The main purpose of this module is to provide a quick and easy way to get/set/call/delete attributes of a Python object from js using the same (or similar) syntax to how the get/set/call/delete is done in Python.
Additionally, because this concept inherently exposes back-end/server-side data and functionality to client-side and could be a security risk if used improperly, some features have been built in to restrict access to certain methods, attributes, and properties.
This module uses websockets to communicate between the front-end (Javascript) and back-end (Python). The advantage of this over HTTP requests is that while HTTP requests must always originate from the front-end, websocket messages can be sent by either the front-end or the back-end.
While with traditional HTTP, you may need to "poll" frequently to ask the back-end if any fresh data is available, with websockets we can simply tell the back-end to update us with any updates to the data, and then the back-end will send messages whenever data is updated.
This concept has been implemented based off of the traditional MQTT style data-flow, where each client can subscribe to topics (we are calling them channels) and then will receive all messages posted to the topic.
from wispyobserver import RPCWebApp
class Sample(object):
a = 1
"""sample attribute which can be get/set/deleted from javascript"""
tracked_channel_values = {
"a": 1,
"b": "test",
"c": [2,3],
"d": {"e": 5}
}
def test_sum(self, a, b):
"""test function which can be called from javascript"""
print(f"summing {a} + {b}")
return a + b
sample = Sample()
rpc_web_app = RPCWebApp(sample=sample)
rpc_web_app.run()var sample = rpc.sample; // get a JS Proxy which interacts with the backend
sample.rpcWebsocket.listChannels().then(channelNames=>console.log("channelNames=", channelNames)); // get the names of available channels
sample.rpcWebsocket.addChannelHandlers({
"a": v=>console.log("received value for a=", v),
"b": v=>console.log("received value for b=", v),
}, true) // add functions to handle receiving channel update values pushed from Python
sample.test_sum(4, 5).then(v=>console.log("test_sum returned", v)); // call test_sum and attach a callback to the Promise to the result
sample.a = 2; // sets the attribute sample.a on the back end
sample.a.then(v=>console.log("a=",v)); // retrieve the attribute sample.a<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>RPC Web App</title>
</head>
<body>
<!-- add an svg icon to the browser tab -->
<svg-favicon color="blue">
<circle r="5" cx="5" cy="5" style="fill:$color;"></circle>
</svg-favicon>
<!-- define custom parameterized templates to reuse chunks of html in this document-->
<custom-template type="template-form" name="default_name" value="5">
<h5>$name</h5>
<input type="text" placeholder="enter value for $name">
<input type="number" value="$value">
<pre>Inner HTML = $innerHTML</pre>
<hr>
</custom-template>
<custom-template type="flexible-el" name="default_name" value="5">
<div style="display:flex">
<div style="flex:20%">$#a</div><!--reference element with subID="a"" -->
<div style="flex:20%">$#a</div>
<div style="flex:20%">$#b</div>
</div>
</custom-template>
<!-- use the custom element type defined by the custom-template elements above-->
<template-form type="template-form" name="test" value="7">TEST</template-form>
<template-form name="test2">ABC</template-form>
<template-form type="template-form"value="17">DEF</template-form>
<flexible-el>
<button subID="a" tooltip="this is a button#def">A</button><!-- try hovering over this element -->
<button subID="b">b</button>
</flexible-el>
</body>
<script src="../wispyjs/rpc.js"></script><!-- this script is critical to accessing python objects-->
<script src="../wispyjs/page.js"></script><!-- simple tools for accessing and modifying HTML elements -->
<script src="../wispyjs/httpReq.js"></script><!-- simple tools for making http requests -->
<script src="../wispyjs/tooltip.js"></script><!-- tools for adding hover help text -->
<script src="../wispyjs/customElements/svg-favicon.js"></script><!-- quickly add a favicon vector icon to browser tab -->
<script src="../wispyjs/customElements/custom-element.js"></script><!-- create and use custom html element templates -->
</html>multiprocessed: true # whether to run the web application in a child process
base_path: /test # base path to prefix all routes with
# parameters passed into tornado.web.Application.__init__
port: 80 # port to host webpage on (80 is public http port, 443 is public https port, 5000 is common development port)
debug: false # similar to autoreload, whether to monitor for changes to source files
default_host: 0.0.0.0 # default host for incoming request's hosts
address: "" # the address on which the server is listening
autoreload: false # whether to monitor for changes to source files
websocket_max_message_size: 104857600 # the maximum data size of a websocket message, in bytes
wispy_static_path: wispyjs # the path at which to serve th wispyobserver/js folder
static: # parameters configuring static file handlers
homepage: static/index.html # path to redirect to when the base_path is loaded
folders: # str, list, or dict mapping routes to folders whose contents should be served
static: ./static # results in the "./static" folder being served at the /base_path/static path
upload: # parameters used to configure upload handlers
folders: # folders to allow uploading into
uploads: ./uploads # results in the "./upload" folder accepting inputs at the /base_path/uploads path
rules:
accepted_filetypes: all # list of filetypes that are allowed to be uploaded to
get_names_allowed: true # whether to return the filenames of the contents of the uploads folders
file_creation_whitelist: # rules used to whitelist allowed upload paths when the file does not already exist
- "*/server/uploads/*.png"
file_overwrite_whitelist: # rules used to whitelist allowed upload paths when the file does already exist
- "*/server/uploads/*.png"
logging_config: # configure python logging for debugging
logger: web # name of the logger the web app should use
level: DEBUG # level of logging to set the logger to
stream: true # whether to stream logs to STDOUT
synchronous_task_lock: # configure a synchronous task lock object used to work on web requests concurrently
max_workers: null # maximum number of concurrent workers allowed
func_caller: # configuration for a function caller created from the synchronous task lock object
locking_default: false # whether functions should be locking by default (only one locking method can run at once)
synchronous_default: true # whether functions should be synchronous by default or instead be executed in a thread pool
max_start_delay_default: 3 # the maximum allowed time from when a web request is sent to when the worker starts executing
check_return_annotation_default: true # whether to use the method annotations to decide whether to run function synchronously
interface: # configuration used create interfaces for python object being served by the RPC server
include_internals: false # whether to provide access to methods and attributes starting with '_'
include_dunders: false # whether to provide access to methods and attributes starting with '__'
max_interface_depth: 4 # the max depth of object attributes which are served
access_restricting_attributes: # names of attributes which can be added to methods and attributes to restrict access
- "interface_access_restricted"
source_access: true # whether python source code should be served to the front-end
add_attributes: true # whether to add attributes to the objects which are being served with the RPC server
instance_configs: {} # special configs to override defaults with for specific python instances- HTML -> click
- JS -> websocket message
- (Python web process) tornado application -> RPCPipeConnection -> connection or Queue message
- (Python main process worker thread) RPCPipeConnection -> interface -> SynchronousTaskLock -> executor
- (new Python main process worker thread) -> instance -> function call -> result -> all the way back the same path
- (Python main process) Python instance -> Queue message
- (Python web process) tornado application -> websocket message
- JS -> channel_handler -> JS -> update HTML