Skip to content

modularizer/wispyobserver

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

17 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

WebSocket Based Python Object Server

Features

  • 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

RPC requests (Python from front-end)

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.

Websocket Channels (Subscribing to data updates)

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.

Python Quickstart

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()

Javascript Quickstart

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

HTML Quickstart

<!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>

Server Config

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

Data Path

Function Calls

  • 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

Channel Updates

  • (Python main process) Python instance -> Queue message
  • (Python web process) tornado application -> websocket message
  • JS -> channel_handler -> JS -> update HTML