├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── docs ├── index.html └── jyserver │ ├── Bottle.html │ ├── Django.html │ ├── FastAPI.html │ ├── Flask.html │ ├── Server.html │ ├── index.html │ └── jscript.html ├── examples ├── Bottle │ └── clock.py ├── Django │ ├── clock │ │ ├── clock │ │ │ ├── __init__.py │ │ │ ├── asgi.py │ │ │ ├── settings.py │ │ │ ├── urls.py │ │ │ ├── views.py │ │ │ └── wsgi.py │ │ ├── db.sqlite3 │ │ └── manage.py │ ├── hello_world │ │ ├── urls.py │ │ └── views.py │ └── templates │ │ └── hello_world.html ├── FastAPI │ └── clock.py ├── Flask │ ├── clock.py │ ├── simple.py │ └── templates │ │ ├── clock.html │ │ └── flask-simple.html └── Server │ ├── clock │ └── clock.py │ ├── increment │ ├── index.html │ └── server.py │ └── steps │ ├── index.html │ └── srv.py ├── jyserver ├── Bottle.py ├── Django.py ├── FastAPI.py ├── Flask.py ├── Server.py ├── __init__.py ├── jscript.py ├── jyserver-min.js └── jyserver.js ├── requirements.txt ├── setup.py └── tests ├── context.py ├── template_vars.py ├── templates ├── flask-simple.html └── flask1.html ├── test_bottle.py ├── test_class_main.py ├── test_clock.py ├── test_clock_counter.py ├── test_clock_nocookies.py ├── test_django.py ├── test_flask.py ├── test_main.py ├── test_throws.py └── test_vars.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | build 3 | dist 4 | .vscode 5 | .p* 6 | scratch -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 by Fernando Trias 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software 4 | and associated documentation files (the "Software"), to deal in the Software without restriction, 5 | including without limitation the rights to use, copy, modify, merge, publish, distribute, 6 | sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 7 | furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or 10 | substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 13 | BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 14 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 15 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: jyserver/jyserver-min.js html wheel 2 | 3 | init: 4 | pip install -r requirements.txt 5 | 6 | test: 7 | py.test tests 8 | 9 | wheel: 10 | rm -rf dist 11 | python setup.py sdist bdist_wheel 12 | 13 | clean: 14 | rm -rf dist 15 | rm -r *.egg-info 16 | 17 | install: wheel 18 | pip install --force dist/jyserver-*-py3-none-any.whl 19 | 20 | upload: wheel 21 | twine upload dist/* 22 | 23 | html: docs 24 | # pdoc --html --html-dir docs --all-submodules jyserver 25 | pdoc3 --html -o docs --force jyserver 26 | 27 | # use https://developers.google.com/closure/compiler/docs/gettingstarted_app 28 | jyserver/jyserver-min.js: jyserver/jyserver.js 29 | java -jar scratch/closure-compiler-v20200406.jar --js $< --js_output_file $@ 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jyserver Web Framework with Pythonic Javascript Syntax 2 | 3 | Jyserver is a framework for simplifying the creation of font ends for apps and 4 | kiosks by providing real-time access to the browser's DOM and Javascript from 5 | the server using Python syntax. It also provides access to the Python code from 6 | the browser's Javascript. It can be used stand-alone or with other 7 | frameworks such as: 8 | 9 | * Flask 10 | * Bottle 11 | * FastAPI 12 | * Django 13 | 14 | jyserver uses Python's dynamic syntax evaluation so that you can write 15 | Python code that will dynamically be converted to JS and executed on the 16 | browser. On the browser end, it uses JS's dynamic Proxy object to rewrite JS 17 | code for execution by the server. All of this is done transparently without any 18 | additional libraries or code. See examples below. 19 | 20 | Documentation: [Class documentation](https://ftrias.github.io/jyserver/) 21 | 22 | Git (and examples): [github:ftrias/jyserver](https://github.com/ftrias/jyserver) 23 | 24 | Tutorial: [Dev.to article](https://dev.to/ftrias/simple-kiosk-framework-in-python-2ane) 25 | 26 | Tutorial Flask/Bottle: [Dev.to Flask article](https://dev.to/ftrias/access-js-dom-from-flask-app-using-jyserver-23h9) 27 | 28 | The examples below show the same app that displays a running timer. The timer is controlled from the 29 | server by the main() task, which usually runs on its own thread. DOM items are accessed via the 30 | `self.js.dom` object using Javascript notation. The app has a button called `reset` that calls a 31 | server function also called `reset()`. 32 | 33 | The main differences in the jyserver code between Flask, Django, Bottle and FastAPI are in the 34 | syntax with the decorator `@js.use`. Otherwise, the jyserver code is identical. 35 | 36 | ## Standalone Example: 37 | 38 | ```python 39 | from jserver import Client, Server 40 | class App(Client): 41 | def __init__(self): 42 | # For simplicity, this is the web page we are rendering. 43 | # The module will add the relevant JS code to 44 | # make it all work. You can also use an html file. 45 | self.html = """ 46 |

TIME

47 | 49 | """ 50 | 51 | # Called by onclick 52 | def reset(self): 53 | # reset counter so elapsed time is 0 54 | self.start0 = time.time() 55 | # executed on client 56 | self.js.dom.time.innerHTML = "{:.1f}".format(0) 57 | 58 | # If there is a "main" function, it gets executed. Program 59 | # ends when the function ends. If there is no main, then 60 | # server runs forever. 61 | def main(self): 62 | # start counter so elapsed time is 0 63 | self.start0 = time.time() 64 | while True: 65 | # get current elapsed time, rounded to 0.1 seconds 66 | t = "{:.1f}".format(time.time() - self.start0) 67 | # update the DOM on the client 68 | self.js.dom.time.innerHTML = t 69 | time.sleep(0.1) 70 | 71 | httpd = Server(App) 72 | print("serving at port", httpd.port) 73 | httpd.start() 74 | ``` 75 | 76 | ## Flask Example: 77 | 78 | ```html 79 |

TIME

80 | 81 | ``` 82 | 83 | ```python 84 | import jyserver.Flask as js 85 | import time 86 | from flask import Flask, render_template, request 87 | 88 | app = Flask(__name__) 89 | 90 | @js.use(app) 91 | class App(): 92 | def reset(self): 93 | self.start0 = time.time() 94 | self.js.dom.time.innerHTML = "{:.1f}".format(0) 95 | 96 | @js.task 97 | def main(self): 98 | self.start0 = time.time() 99 | while True: 100 | t = "{:.1f}".format(time.time() - self.start0) 101 | self.js.dom.time.innerHTML = t 102 | time.sleep(0.1) 103 | 104 | @app.route('/') 105 | def index_page(name=None): 106 | App.main() 107 | return App.render(render_template('flask-simple.html') 108 | ``` 109 | 110 | ## Bottle example 111 | 112 | A Bottle application using the built-in server can only be single threaded and thus 113 | all features may not work as expected. Most significantly, you cannot 114 | evaluate Javascript expressions from server callbacks. This limitation 115 | is not present if using a multi-threaded server such as tornado. 116 | 117 | ```python 118 | from bottle import route, run 119 | import jyserver.Bottle as js 120 | import time 121 | 122 | @js.use 123 | class App(): 124 | def reset(self): 125 | self.start0 = time.time() 126 | 127 | @js.task 128 | def main(self): 129 | self.start0 = time.time() 130 | while True: 131 | t = "{:.1f}".format(time.time() - self.start0) 132 | self.js.dom.time.innerHTML = t 133 | time.sleep(0.1) 134 | 135 | @route('/') 136 | def index(): 137 | html = """ 138 |

WHEN

139 | 140 | """ 141 | App.main() 142 | return App.render(html) 143 | 144 | run(host='localhost', port=8080) 145 | ``` 146 | 147 | ## FastAPI example 148 | 149 | ```python 150 | import jyserver.FastAPI as js 151 | import time 152 | 153 | from fastapi import FastAPI 154 | from fastapi.responses import HTMLResponse 155 | 156 | app = FastAPI(__name__) 157 | 158 | @js.use(app) 159 | class App(): 160 | def reset(self): 161 | self.start0 = time.time() 162 | self.js.dom.time.innerHTML = "{:.1f}".format(0) 163 | 164 | @js.task 165 | def main(self): 166 | self.start0 = time.time() 167 | while True: 168 | t = "{:.1f}".format(time.time() - self.start0) 169 | self.js.dom.time.innerHTML = t 170 | time.sleep(0.1) 171 | 172 | @app.get('/', response_class=HTMLResponse) 173 | async def index_page(): 174 | App.main() 175 | html = """ 176 |

TIME

177 | 178 | """ 179 | return App.render(html) 180 | ``` 181 | 182 | ## Django example 183 | 184 | ```python 185 | from django.http import HttpResponse 186 | import jyserver.Django as js 187 | import time 188 | 189 | @js.use 190 | class App(): 191 | def reset(self): 192 | self.start0 = time.time() 193 | self.js.dom.time.innerHTML = "{:.1f}".format(0) 194 | 195 | @js.task 196 | def main(self): 197 | self.start0 = time.time() 198 | while True: 199 | t = "{:.1f}".format(time.time() - self.start0) 200 | self.js.dom.time.innerHTML = t 201 | time.sleep(0.1) 202 | 203 | def hello_world(request): 204 | App.main() 205 | html = """ 206 |

WHEN

207 | 208 | 209 | """ 210 | return App.render(HttpResponse(html)) 211 | ``` 212 | 213 | In `urls.py` add this path: 214 | 215 | ```python 216 | from jyserver.Django import process 217 | ... 218 | url(r'^_process_srv0$', process, name='process'), 219 | ``` 220 | 221 | ## Internals 222 | 223 | How does this work? In the standalone example, the process is below. 224 | Flask/Bottle/Django is identical except for the httpd server. 225 | 226 | 1. The server will listen for new http requests. 227 | 228 | 2. When "/" is requested, jyserver will insert special Javascript code into the 229 | HTML that enables communication before sending it to the browser. This code 230 | creates the `server` Proxy object. 231 | 232 | 3. This injected code will cause the browser to send an asynchronous http 233 | request to the server asking for new commands for the browser to execute. 234 | Then it waits for a response in the background. Requests are sent via 235 | POST on /_process_srv0, which the server intercepts. 236 | 237 | 4. When the user clicks on the button `reset`, the `server` Proxy object is 238 | called. It will extract the method name--in this case `reset`--and then make 239 | an http request to the server to execute that statement. 240 | 241 | 5. The server will receive this http request, look at the App class, find a 242 | method with that name and execute it. 243 | 244 | 6. The executed method `reset()` first increases the variable `start0`. Then it 245 | begins building a Javascript command by using the special `self.js` command. 246 | `self.js` uses Python's dynamic language features `__getattr__`, 247 | `__setattr__`, etc. to build Javascript syntax on the fly. 248 | 249 | 7. When this "dynamic" statement get assigned a value (in our case `"0.0"`), it 250 | will get converted to Javascript and sent to the browser, which has been 251 | waiting for new commands in step 3. The statement will look like: 252 | `document.getElementById("time").innerHTML = "0.0"` 253 | 254 | 8. The browser will get the statement, evaluate it and return the results to the 255 | server. Then the browser will query for new commands in the background. 256 | 257 | It seems complicated but this process usually takes less than a 0.01 seconds. If 258 | there are multiple statements to execute, they get queued and processed 259 | together, which cuts back on the back-and-forth chatter. 260 | 261 | All communication is initiated by the browser. The server only listens for 262 | special GET and POST requests. 263 | 264 | ## Overview of operation 265 | 266 | The browser initiates all communcation. The server listens for connections and 267 | sends respnses. Each page request is processed in its own thread so results may 268 | finish out of order and any waiting does not stall either the browser or the 269 | server. 270 | 271 | | Browser | Server | 272 | |-----------|-----------| 273 | | Request pages | Send pages with injected Javascript | 274 | | Query for new commands | Send any queued commands | 275 | | As commands finish, send back results | Match results with commands | 276 | | Send server statements for evaluation; wait for results | Executes then and sends back results | 277 | 278 | When the browser queries for new commands, the server returns any pending 279 | commands that the browser needs to execute. If there are no pending commands, it 280 | waits for 5-10 seconds for new commands to queue before closing the connection. 281 | The browser, upon getting an empty result will initiate a new connection to 282 | query for results. Thus, although there is always a connection open between the 283 | browser and server, this connection is reset every 5-10 seconds to avoid a 284 | timeout. 285 | 286 | ## Other features 287 | 288 | ### Assign callables in Python. 289 | 290 | Functions are treated as first-class objects and can be assigned. 291 | 292 | ```python 293 | class App(Client): 294 | def stop(self): 295 | self.running = False 296 | self.js.dom.b2.onclick = self.restart 297 | def restart(self): 298 | self.running = True 299 | self.js.dom.b2.onclick = self.stop 300 | ``` 301 | 302 | If a `main` function is given, it is executed. When it finishes, the server is 303 | terminated. If no `main` function is given, the server waits for requests in an 304 | infinite loop. 305 | 306 | ### Lazy evaluation provides live data 307 | 308 | Statements are evaluated lazily by `self.js`. This means that they are executed 309 | only when they are resolved to an actual value, which can cause some statements 310 | to be evaluated out of order. For example, consider: 311 | 312 | ```python 313 | v = self.js.var1 314 | self.js.var1 = 10 315 | print(v) 316 | ``` 317 | 318 | This will always return `10` no matter what `var1` is initially. This is 319 | because the assignment `v = self.js.var1` assigns a Javascript object and not 320 | the actual value. The object is sent to the browser to be evaluated only when 321 | it is used by an operation. Every time you use `v` in an operation, it will be 322 | sent to the browser for evaluation. In this way, it provides a live link to the 323 | data. 324 | 325 | This behavior can be changed by calling `v = self.js.var1.eval()`, casting it 326 | such as `v = int(self.js.var)` or performing some operation such as adding as in 327 | `v = self.js.var + 10`. 328 | 329 | ## Installation 330 | 331 | Available using pip or conda 332 | 333 | ```bash 334 | pip install jyserver 335 | ``` 336 | 337 | Source code available on [github:ftrias/jyserver](https://github.com/ftrias/jyserver) 338 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Please follow this link.

8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/jyserver/Bottle.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | jyserver.Bottle API documentation 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |
23 |

Module jyserver.Bottle

24 |
25 |
26 |

Module for using jyserver in Bottle. This module provides two new 27 | decorators.

28 |

Decorators

29 |
    30 |
  • 31 |

    @use

    32 |

    Link an application object to the Bottle app

    33 |
  • 34 |
  • 35 |

    @task

    36 |

    Helper that wraps a function inside a separate thread so that 37 | it can execute concurrently.

    38 |
  • 39 |
40 |

Example

41 |
from bottle import route, run
 42 | import jyserver.Bottle as js
 43 | import time
 44 | 
 45 | @js.use
 46 | class App():
 47 |     def reset(self):
 48 |         self.start0 = time.time()
 49 | 
 50 |     @js.task
 51 |     def main(self):
 52 |         self.start0 = time.time()
 53 |         while True:
 54 |             t = "{:.1f}".format(time.time() - self.start0)
 55 |             self.js.dom.time.innerHTML = t
 56 |             time.sleep(0.1)
 57 | 
 58 | @route('/')
 59 | def index():
 60 |     html = """
 61 |         <p id="time">WHEN</p>
 62 |         <button id="b1" onclick="server.reset()">Reset</button>
 63 |     """
 64 |     App.main()
 65 |     return App.render(html)
 66 | 
 67 | run(host='localhost', port=8080)
 68 | 
69 |
70 | 71 | Expand source code 72 | 73 |
'''
 74 | Module for using jyserver in Bottle. This module provides two new
 75 | decorators.
 76 | 
 77 | Decorators
 78 | -----------
 79 | 
 80 | * @use
 81 | 
 82 |     Link an application object to the Bottle app
 83 | 
 84 | * @task
 85 | 
 86 |     Helper that wraps a function inside a separate thread so that
 87 |     it can execute concurrently.
 88 | 
 89 | Example
 90 | -------------
 91 | ```python
 92 | from bottle import route, run
 93 | import jyserver.Bottle as js
 94 | import time
 95 | 
 96 | @js.use
 97 | class App():
 98 |     def reset(self):
 99 |         self.start0 = time.time()
100 | 
101 |     @js.task
102 |     def main(self):
103 |         self.start0 = time.time()
104 |         while True:
105 |             t = "{:.1f}".format(time.time() - self.start0)
106 |             self.js.dom.time.innerHTML = t
107 |             time.sleep(0.1)
108 | 
109 | @route('/')
110 | def index():
111 |     html = """
112 |         <p id="time">WHEN</p>
113 |         <button id="b1" onclick="server.reset()">Reset</button>
114 |     """
115 |     App.main()
116 |     return App.render(html)
117 | 
118 | run(host='localhost', port=8080)
119 | ```
120 | '''
121 | 
122 | from bottle import route, request
123 | 
124 | import json
125 | import jyserver
126 | import threading
127 | 
128 | def task(func):
129 |     '''
130 |     Decorator wraps the function in a separate thread for concurrent
131 |     execution.
132 |     '''
133 |     def wrapper(*args):
134 |         server_thread = threading.Thread(target=func, args=(args), daemon=True)
135 |         server_thread.start()
136 |     return wrapper
137 | 
138 | def use(appClass):
139 |     '''
140 |     Link a class to an app object.
141 |     '''
142 |     global context
143 |     context = jyserver.ClientContext(appClass)
144 | 
145 |     @route('/_process_srv0', method='POST')
146 |     def process():
147 |         if request.method == 'POST':
148 |             result = context.processCommand(request.json)
149 |             if result is None:
150 |                 return ''
151 |             return result
152 |     return context
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |

Functions

161 |
162 |
163 | def task(func) 164 |
165 |
166 |

Decorator wraps the function in a separate thread for concurrent 167 | execution.

168 |
169 | 170 | Expand source code 171 | 172 |
def task(func):
173 |     '''
174 |     Decorator wraps the function in a separate thread for concurrent
175 |     execution.
176 |     '''
177 |     def wrapper(*args):
178 |         server_thread = threading.Thread(target=func, args=(args), daemon=True)
179 |         server_thread.start()
180 |     return wrapper
181 |
182 |
183 |
184 | def use(appClass) 185 |
186 |
187 |

Link a class to an app object.

188 |
189 | 190 | Expand source code 191 | 192 |
def use(appClass):
193 |     '''
194 |     Link a class to an app object.
195 |     '''
196 |     global context
197 |     context = jyserver.ClientContext(appClass)
198 | 
199 |     @route('/_process_srv0', method='POST')
200 |     def process():
201 |         if request.method == 'POST':
202 |             result = context.processCommand(request.json)
203 |             if result is None:
204 |                 return ''
205 |             return result
206 |     return context
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 | 236 |
237 | 240 | 241 | -------------------------------------------------------------------------------- /docs/jyserver/Django.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | jyserver.Django API documentation 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |
23 |

Module jyserver.Django

24 |
25 |
26 |

Module for using jyserver in Django. This module provides two new 27 | decorators.

28 |

Decorators

29 |
    30 |
  • 31 |

    @use

    32 |

    Link an application object to the Django app

    33 |
  • 34 |
  • 35 |

    @task

    36 |

    Helper that wraps a function inside a separate thread so that 37 | it can execute concurrently.

    38 |
  • 39 |
40 |

Example (assumes working setup)

41 |
from django.shortcuts import render
 42 | import jyserver.Django as js
 43 | import time
 44 | 
 45 | @js.use
 46 | class App():
 47 |     def reset(self):
 48 |         self.start0 = time.time()
 49 |         self.js.dom.time.innerHTML = "{:.1f}".format(0)
 50 | 
 51 |     @js.task
 52 |     def main(self):
 53 |         self.start0 = time.time()
 54 |         while True:
 55 |             t = "{:.1f}".format(time.time() - self.start0)
 56 |             self.js.dom.time.innerHTML = t
 57 |             time.sleep(0.1)
 58 | 
 59 | def hello_world(request):
 60 |     App.main()
 61 |     return App.render(render(request, 'hello_world.html', {}))
 62 | 
63 |

In urls.py add this path:

64 |
from jyserver.Django import process
 65 | ...
 66 |     url(r'^_process_srv0$', process, name='process'),
 67 | 
68 |
69 | 70 | Expand source code 71 | 72 |
'''
 73 | Module for using jyserver in Django. This module provides two new
 74 | decorators.
 75 | 
 76 | Decorators
 77 | -----------
 78 | 
 79 | * @use
 80 | 
 81 |     Link an application object to the Django app
 82 | 
 83 | * @task
 84 | 
 85 |     Helper that wraps a function inside a separate thread so that
 86 |     it can execute concurrently.
 87 | 
 88 | Example (assumes working setup)
 89 | -------------
 90 | ```python
 91 | from django.shortcuts import render
 92 | import jyserver.Django as js
 93 | import time
 94 | 
 95 | @js.use
 96 | class App():
 97 |     def reset(self):
 98 |         self.start0 = time.time()
 99 |         self.js.dom.time.innerHTML = "{:.1f}".format(0)
100 | 
101 |     @js.task
102 |     def main(self):
103 |         self.start0 = time.time()
104 |         while True:
105 |             t = "{:.1f}".format(time.time() - self.start0)
106 |             self.js.dom.time.innerHTML = t
107 |             time.sleep(0.1)
108 | 
109 | def hello_world(request):
110 |     App.main()
111 |     return App.render(render(request, 'hello_world.html', {}))
112 | ```
113 | 
114 | In `urls.py` add this path:
115 | 
116 | ```python
117 | from jyserver.Django import process
118 | ...
119 |     url(r'^_process_srv0$', process, name='process'),
120 | ```
121 | '''
122 | 
123 | from django.shortcuts import render
124 | from django.http import HttpResponse
125 | from django.views.decorators.csrf import csrf_exempt
126 | 
127 | import json
128 | import jyserver
129 | import threading
130 | 
131 | def task(func):
132 |     '''
133 |     Decorator wraps the function in a separate thread for concurrent
134 |     execution.
135 |     '''
136 |     def wrapper(*args):
137 |         server_thread = threading.Thread(target=func, args=args, daemon=True)
138 |         server_thread.start()
139 |     return wrapper
140 | 
141 | @csrf_exempt
142 | def process(request):
143 |     '''
144 |     Used to process browser requests. Must be added to your `urls.py`
145 |     to process `/_process_srv0` as in:
146 |     ```
147 |         url(r'^_process_srv0$', process, name='process'),
148 |     ```
149 |     '''
150 |     if request.method == 'POST':
151 |         req = json.loads(request.body)
152 |         result = context.processCommand(req)
153 |         if result is None:
154 |             return HttpResponse('')
155 |         return HttpResponse(result)
156 |     else:
157 |         return HttpResponse("GET reqeust not allowed")
158 | 
159 | def use(appClass):
160 |     '''
161 |     Link a class to an app object. Pass Flask's `app` object.
162 |     '''
163 |     global context
164 |     context = jyserver.ClientContext(appClass)
165 |     context.render = context.render_django
166 |     return context
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |

Functions

175 |
176 |
177 | def process(request) 178 |
179 |
180 |

Used to process browser requests. Must be added to your urls.py 181 | to process /_process_srv0 as in:

182 |
    url(r'^_process_srv0$', process, name='process'),
183 | 
184 |
185 | 186 | Expand source code 187 | 188 |
@csrf_exempt
189 | def process(request):
190 |     '''
191 |     Used to process browser requests. Must be added to your `urls.py`
192 |     to process `/_process_srv0` as in:
193 |     ```
194 |         url(r'^_process_srv0$', process, name='process'),
195 |     ```
196 |     '''
197 |     if request.method == 'POST':
198 |         req = json.loads(request.body)
199 |         result = context.processCommand(req)
200 |         if result is None:
201 |             return HttpResponse('')
202 |         return HttpResponse(result)
203 |     else:
204 |         return HttpResponse("GET reqeust not allowed")
205 |
206 |
207 |
208 | def task(func) 209 |
210 |
211 |

Decorator wraps the function in a separate thread for concurrent 212 | execution.

213 |
214 | 215 | Expand source code 216 | 217 |
def task(func):
218 |     '''
219 |     Decorator wraps the function in a separate thread for concurrent
220 |     execution.
221 |     '''
222 |     def wrapper(*args):
223 |         server_thread = threading.Thread(target=func, args=args, daemon=True)
224 |         server_thread.start()
225 |     return wrapper
226 |
227 |
228 |
229 | def use(appClass) 230 |
231 |
232 |

Link a class to an app object. Pass Flask's app object.

233 |
234 | 235 | Expand source code 236 | 237 |
def use(appClass):
238 |     '''
239 |     Link a class to an app object. Pass Flask's `app` object.
240 |     '''
241 |     global context
242 |     context = jyserver.ClientContext(appClass)
243 |     context.render = context.render_django
244 |     return context
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 | 275 |
276 | 279 | 280 | -------------------------------------------------------------------------------- /docs/jyserver/FastAPI.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | jyserver.FastAPI API documentation 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |
23 |

Module jyserver.FastAPI

24 |
25 |
26 |

Module for using jyserver in FastAPI. This module provides two new 27 | decorators.

28 |

Decorators

29 |
    30 |
  • 31 |

    @use

    32 |

    Link an application object to the Flask app

    33 |
  • 34 |
  • 35 |

    @task

    36 |

    Helper that wraps a function inside a separate thread so that 37 | it can execute concurrently.

    38 |
  • 39 |
40 |

Example

41 |
import jyserver.FastAPI as js
 42 | import time
 43 | 
 44 | from fastapi import FastAPI
 45 | from fastapi.responses import HTMLResponse
 46 | 
 47 | app = FastAPI(__name__)
 48 | 
 49 | @js.use(app)
 50 | class App():
 51 |     def reset(self):
 52 |         self.start0 = time.time()
 53 |         self.js.dom.time.innerHTML = "{:.1f}".format(0)
 54 | 
 55 |     @js.task
 56 |     def main(self):
 57 |         self.start0 = time.time()
 58 |         while True:
 59 |             t = "{:.1f}".format(time.time() - self.start0)
 60 |             self.js.dom.time.innerHTML = t
 61 |             time.sleep(0.1)
 62 | 
 63 | @app.get('/', response_class=HTMLResponse)
 64 | async def index_page():
 65 |     App.main()
 66 |     html =  """
 67 | <p id="time">TIME</p>
 68 | <button id="reset" onclick="server.reset()">Reset</button>
 69 | """
 70 |     return App.render(html)
 71 | 
72 |
73 | 74 | Expand source code 75 | 76 |
'''
 77 | Module for using jyserver in FastAPI. This module provides two new
 78 | decorators.
 79 | 
 80 | Decorators
 81 | -----------
 82 | 
 83 | * @use
 84 | 
 85 |     Link an application object to the Flask app
 86 | 
 87 | * @task
 88 | 
 89 |     Helper that wraps a function inside a separate thread so that
 90 |     it can execute concurrently.
 91 | 
 92 | Example
 93 | -------------
 94 | ```python
 95 | import jyserver.FastAPI as js
 96 | import time
 97 | 
 98 | from fastapi import FastAPI
 99 | from fastapi.responses import HTMLResponse
100 | 
101 | app = FastAPI(__name__)
102 | 
103 | @js.use(app)
104 | class App():
105 |     def reset(self):
106 |         self.start0 = time.time()
107 |         self.js.dom.time.innerHTML = "{:.1f}".format(0)
108 | 
109 |     @js.task
110 |     def main(self):
111 |         self.start0 = time.time()
112 |         while True:
113 |             t = "{:.1f}".format(time.time() - self.start0)
114 |             self.js.dom.time.innerHTML = t
115 |             time.sleep(0.1)
116 | 
117 | @app.get('/', response_class=HTMLResponse)
118 | async def index_page():
119 |     App.main()
120 |     html =  """
121 | <p id="time">TIME</p>
122 | <button id="reset" onclick="server.reset()">Reset</button>
123 | """
124 |     return App.render(html)
125 | ```
126 | '''
127 | 
128 | from fastapi import FastAPI, Request, Response
129 | from fastapi.responses import HTMLResponse
130 | from pydantic import BaseModel
131 | 
132 | import jyserver
133 | import threading
134 | 
135 | def task(func):
136 |     '''
137 |     Decorator wraps the function in a separate thread for concurrent
138 |     execution.
139 |     '''
140 |     def wrapper(*args):
141 |         server_thread = threading.Thread(target=func, args=args, daemon=True)
142 |         server_thread.start()
143 |     return wrapper
144 | 
145 | def use(myapp):
146 |     '''
147 |     Link a class to an app object. Pass `app` object.
148 |     '''
149 |     def decorator(appClass):
150 |         global context
151 |         context = jyserver.ClientContext(appClass)
152 | 
153 |         @myapp.post('/_process_srv0')
154 |         async def process(item: Request):
155 |             req = await item.json()
156 |             result = context.processCommand(req)
157 |             if result is None: result = ''
158 |             return Response(content=result, media_type="text/plain")
159 |         return context
160 | 
161 |     return decorator
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |

Functions

170 |
171 |
172 | def task(func) 173 |
174 |
175 |

Decorator wraps the function in a separate thread for concurrent 176 | execution.

177 |
178 | 179 | Expand source code 180 | 181 |
def task(func):
182 |     '''
183 |     Decorator wraps the function in a separate thread for concurrent
184 |     execution.
185 |     '''
186 |     def wrapper(*args):
187 |         server_thread = threading.Thread(target=func, args=args, daemon=True)
188 |         server_thread.start()
189 |     return wrapper
190 |
191 |
192 |
193 | def use(myapp) 194 |
195 |
196 |

Link a class to an app object. Pass app object.

197 |
198 | 199 | Expand source code 200 | 201 |
def use(myapp):
202 |     '''
203 |     Link a class to an app object. Pass `app` object.
204 |     '''
205 |     def decorator(appClass):
206 |         global context
207 |         context = jyserver.ClientContext(appClass)
208 | 
209 |         @myapp.post('/_process_srv0')
210 |         async def process(item: Request):
211 |             req = await item.json()
212 |             result = context.processCommand(req)
213 |             if result is None: result = ''
214 |             return Response(content=result, media_type="text/plain")
215 |         return context
216 | 
217 |     return decorator
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 | 247 |
248 | 251 | 252 | -------------------------------------------------------------------------------- /docs/jyserver/Flask.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | jyserver.Flask API documentation 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |
23 |

Module jyserver.Flask

24 |
25 |
26 |

Module for using jyserver in Flask. This module provides two new 27 | decorators.

28 |

Decorators

29 |
    30 |
  • 31 |

    @use

    32 |

    Link an application object to the Flask app

    33 |
  • 34 |
  • 35 |

    @task

    36 |

    Helper that wraps a function inside a separate thread so that 37 | it can execute concurrently.

    38 |
  • 39 |
40 |

Example

41 |
<p id="time">TIME</p>
 42 | <button id="reset" onclick="server.reset()">Reset</button>
 43 | 
44 |

```python 45 | import jyserver.Flask as js 46 | import time 47 | from flask import Flask, render_template, request

48 |

app = Flask(name)

49 |

@js.use(app) 50 | class App(): 51 | def reset(self): 52 | self.start0 = time.time() 53 | self.js.dom.time.innerHTML = "{:.1f}".format(0)

54 |
@js.task
 55 | def main(self):
 56 |     self.start0 = time.time()
 57 |     while True:
 58 |         t = "{:.1f}".format(time.time() - self.start0)
 59 |         self.js.dom.time.innerHTML = t
 60 |         time.sleep(0.1)
 61 | 
62 |

@app.route('/') 63 | def index_page(name=None): 64 | App.main() 65 | return App.render(render_template('flask-simple.html')

66 |
67 | 68 | Expand source code 69 | 70 |
'''
 71 | Module for using jyserver in Flask. This module provides two new
 72 | decorators.
 73 | 
 74 | Decorators
 75 | -----------
 76 | 
 77 | * @use
 78 | 
 79 |     Link an application object to the Flask app
 80 | 
 81 | * @task
 82 | 
 83 |     Helper that wraps a function inside a separate thread so that
 84 |     it can execute concurrently.
 85 | 
 86 | Example
 87 | -------------
 88 | ```html
 89 | <p id="time">TIME</p>
 90 | <button id="reset" onclick="server.reset()">Reset</button>
 91 | ```
 92 | 
 93 | ```python
 94 | import jyserver.Flask as js
 95 | import time
 96 | from flask import Flask, render_template, request
 97 | 
 98 | app = Flask(__name__)
 99 | 
100 | @js.use(app)
101 | class App():
102 |     def reset(self):
103 |         self.start0 = time.time()
104 |         self.js.dom.time.innerHTML = "{:.1f}".format(0)
105 | 
106 |     @js.task
107 |     def main(self):
108 |         self.start0 = time.time()
109 |         while True:
110 |             t = "{:.1f}".format(time.time() - self.start0)
111 |             self.js.dom.time.innerHTML = t
112 |             time.sleep(0.1)
113 | 
114 | @app.route('/')
115 | def index_page(name=None):
116 |     App.main()
117 |     return App.render(render_template('flask-simple.html')
118 | '''
119 | 
120 | from flask import Flask, request
121 | import json
122 | import jyserver
123 | import threading
124 | 
125 | def task(func):
126 |     '''
127 |     Decorator wraps the function in a separate thread for concurrent
128 |     execution.
129 |     '''
130 |     def wrapper(*args):
131 |         server_thread = threading.Thread(target=func, args=args, daemon=True)
132 |         server_thread.start()
133 |     return wrapper
134 | 
135 | def use(flaskapp):
136 |     '''
137 |     Link a class to an app object. Pass Flask's `app` object.
138 |     '''
139 |     def decorator(appClass):
140 |         global context
141 |         context = jyserver.ClientContext(appClass)
142 | 
143 |         @flaskapp.route('/_process_srv0', methods=['GET', 'POST'])
144 |         def process():
145 |             if request.method == 'POST':
146 |                 req = json.loads(request.data)
147 |                 result = context.processCommand(req)
148 |                 if result is None:
149 |                     return ''
150 |                 return result
151 |             else:
152 |                 return "GET request not allowed"
153 |         return context
154 | 
155 |     return decorator
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |

Functions

164 |
165 |
166 | def task(func) 167 |
168 |
169 |

Decorator wraps the function in a separate thread for concurrent 170 | execution.

171 |
172 | 173 | Expand source code 174 | 175 |
def task(func):
176 |     '''
177 |     Decorator wraps the function in a separate thread for concurrent
178 |     execution.
179 |     '''
180 |     def wrapper(*args):
181 |         server_thread = threading.Thread(target=func, args=args, daemon=True)
182 |         server_thread.start()
183 |     return wrapper
184 |
185 |
186 |
187 | def use(flaskapp) 188 |
189 |
190 |

Link a class to an app object. Pass Flask's app object.

191 |
192 | 193 | Expand source code 194 | 195 |
def use(flaskapp):
196 |     '''
197 |     Link a class to an app object. Pass Flask's `app` object.
198 |     '''
199 |     def decorator(appClass):
200 |         global context
201 |         context = jyserver.ClientContext(appClass)
202 | 
203 |         @flaskapp.route('/_process_srv0', methods=['GET', 'POST'])
204 |         def process():
205 |             if request.method == 'POST':
206 |                 req = json.loads(request.data)
207 |                 result = context.processCommand(req)
208 |                 if result is None:
209 |                     return ''
210 |                 return result
211 |             else:
212 |                 return "GET request not allowed"
213 |         return context
214 | 
215 |     return decorator
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 | 245 |
246 | 249 | 250 | -------------------------------------------------------------------------------- /docs/jyserver/jscript.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | jyserver.jscript API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |

Module jyserver.jscript

23 |
24 |
25 |
26 | 27 | Expand source code 28 | 29 |
import os
30 | dir = os.path.dirname(__file__)
31 | with open(dir + "/jyserver-min.js", "rb") as f:
32 |     JSCRIPT = f.read()
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | 57 |
58 | 61 | 62 | -------------------------------------------------------------------------------- /examples/Bottle/clock.py: -------------------------------------------------------------------------------- 1 | from bottle import route, run 2 | 3 | import jyserver.Bottle as js 4 | import time 5 | 6 | @js.use 7 | class App(): 8 | def reset(self): 9 | self.start0 = time.time() 10 | 11 | @js.task 12 | def main(self): 13 | self.start0 = time.time() 14 | while True: 15 | t = "{:.1f}".format(time.time() - self.start0) 16 | self.js.dom.time.innerHTML = t 17 | time.sleep(0.1) 18 | 19 | @route('/') 20 | def index(): 21 | html = """ 22 |

WHEN

23 | 24 | """ 25 | App.main() 26 | return App.render(html) 27 | 28 | run(host='localhost', port=8080) 29 | -------------------------------------------------------------------------------- /examples/Django/clock/clock/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ftrias/jyserver/66b9946b973166c496075bdb13e189aa9c66c57f/examples/Django/clock/clock/__init__.py -------------------------------------------------------------------------------- /examples/Django/clock/clock/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for clock project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'clock.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /examples/Django/clock/clock/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for clock project. 3 | 4 | Generated by 'django-admin startproject' using Django 4.2.9. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/4.2/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve().parent.parent 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'django-insecure-z#=m=^v_3m&$8&308rwrhhql)ysr21jodz2p%#qn9c*l^-atf6' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | ] 41 | 42 | MIDDLEWARE = [ 43 | 'django.middleware.security.SecurityMiddleware', 44 | 'django.contrib.sessions.middleware.SessionMiddleware', 45 | 'django.middleware.common.CommonMiddleware', 46 | 'django.middleware.csrf.CsrfViewMiddleware', 47 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 48 | 'django.contrib.messages.middleware.MessageMiddleware', 49 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 50 | ] 51 | 52 | ROOT_URLCONF = 'clock.urls' 53 | 54 | TEMPLATES = [ 55 | { 56 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 57 | 'DIRS': [], 58 | 'APP_DIRS': True, 59 | 'OPTIONS': { 60 | 'context_processors': [ 61 | 'django.template.context_processors.debug', 62 | 'django.template.context_processors.request', 63 | 'django.contrib.auth.context_processors.auth', 64 | 'django.contrib.messages.context_processors.messages', 65 | ], 66 | }, 67 | }, 68 | ] 69 | 70 | WSGI_APPLICATION = 'clock.wsgi.application' 71 | 72 | 73 | # Database 74 | # https://docs.djangoproject.com/en/4.2/ref/settings/#databases 75 | 76 | DATABASES = { 77 | 'default': { 78 | 'ENGINE': 'django.db.backends.sqlite3', 79 | 'NAME': BASE_DIR / 'db.sqlite3', 80 | } 81 | } 82 | 83 | 84 | # Password validation 85 | # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators 86 | 87 | AUTH_PASSWORD_VALIDATORS = [ 88 | { 89 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 90 | }, 91 | { 92 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 93 | }, 94 | { 95 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 96 | }, 97 | { 98 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 99 | }, 100 | ] 101 | 102 | 103 | # Internationalization 104 | # https://docs.djangoproject.com/en/4.2/topics/i18n/ 105 | 106 | LANGUAGE_CODE = 'en-us' 107 | 108 | TIME_ZONE = 'UTC' 109 | 110 | USE_I18N = True 111 | 112 | USE_TZ = True 113 | 114 | 115 | # Static files (CSS, JavaScript, Images) 116 | # https://docs.djangoproject.com/en/4.2/howto/static-files/ 117 | 118 | STATIC_URL = 'static/' 119 | 120 | # Default primary key field type 121 | # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field 122 | 123 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 124 | -------------------------------------------------------------------------------- /examples/Django/clock/clock/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | URL configuration for clock project. 3 | 4 | The `urlpatterns` list routes URLs to views. For more information please see: 5 | https://docs.djangoproject.com/en/4.2/topics/http/urls/ 6 | Examples: 7 | Function views 8 | 1. Add an import: from my_app import views 9 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 10 | Class-based views 11 | 1. Add an import: from other_app.views import Home 12 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 13 | Including another URLconf 14 | 1. Import the include() function: from django.urls import include, path 15 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 16 | """ 17 | from django.contrib import admin 18 | from django.urls import path 19 | from jyserver.Django import process 20 | from . import views 21 | 22 | urlpatterns = [ 23 | path('admin/', admin.site.urls), 24 | path('', views.hello_world, name='hello_world'), 25 | path('_process_srv0', process, name='process'), 26 | ] 27 | -------------------------------------------------------------------------------- /examples/Django/clock/clock/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.http import HttpResponse 3 | 4 | import jyserver.Django as js 5 | import time 6 | 7 | @js.use 8 | class App(): 9 | def reset(self): 10 | print("RESET") 11 | self.start0 = time.time() 12 | self.js.dom.time.innerHTML = "{:.1f}".format(0) 13 | 14 | @js.task 15 | def main(self): 16 | self.start0 = time.time() 17 | while True: 18 | t = "{:.1f}".format(time.time() - self.start0) 19 | self.js.dom.time.innerHTML = t 20 | time.sleep(0.1) 21 | 22 | def hello_world(request): 23 | App.main() 24 | html = """ 25 |

WHEN

26 | 27 | 28 | """ 29 | return App.render(HttpResponse(html)) 30 | -------------------------------------------------------------------------------- /examples/Django/clock/clock/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for clock project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'clock.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /examples/Django/clock/db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ftrias/jyserver/66b9946b973166c496075bdb13e189aa9c66c57f/examples/Django/clock/db.sqlite3 -------------------------------------------------------------------------------- /examples/Django/clock/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'clock.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /examples/Django/hello_world/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from hello_world import views 3 | from jyserver.Django import process 4 | 5 | urlpatterns = [ 6 | url(r'^$', views.hello_world, name='hello_world'), 7 | url(r'^_process_srv0$', process, name='process'), 8 | ] 9 | -------------------------------------------------------------------------------- /examples/Django/hello_world/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.shortcuts import render 5 | 6 | import jyserver.Django as js 7 | import time 8 | 9 | @js.use 10 | class App(): 11 | def reset(self): 12 | print("RESET") 13 | self.start0 = time.time() 14 | self.js.dom.time.innerHTML = "{:.1f}".format(0) 15 | 16 | @js.task 17 | def main(self): 18 | self.start0 = time.time() 19 | while True: 20 | t = "{:.1f}".format(time.time() - self.start0) 21 | self.js.dom.time.innerHTML = t 22 | time.sleep(0.1) 23 | 24 | def hello_world(request): 25 | App.main() 26 | return App.render(render(request, 'hello_world.html', {})) 27 | -------------------------------------------------------------------------------- /examples/Django/templates/hello_world.html: -------------------------------------------------------------------------------- 1 |

Hello world!

2 |

0

3 |

0

4 | 5 | -------------------------------------------------------------------------------- /examples/FastAPI/clock.py: -------------------------------------------------------------------------------- 1 | import jyserver.FastAPI as jsf 2 | import time 3 | import uvicorn 4 | 5 | from fastapi import FastAPI 6 | from fastapi.responses import HTMLResponse 7 | 8 | app = FastAPI() 9 | 10 | @jsf.use(app) 11 | class App: 12 | def reset(self): 13 | self.start0 = time.time() 14 | self.js.dom.time.innerHTML = "{:.1f}".format(0) 15 | def stop(self): 16 | self.running = False 17 | self.js.dom.b2.innerHTML = "Restart" 18 | self.js.dom.b2.onclick = self.restart 19 | def restart(self): 20 | self.running = True 21 | self.js.dom.b2.innerHTML = "Pause" 22 | self.js.dom.b2.onclick = self.stop 23 | 24 | @jsf.task 25 | def main(self): 26 | self.running = True 27 | self.start0 = time.time() 28 | for i in range(1000): 29 | if self.running: 30 | t = "{:.1f}".format(time.time() - self.start0) 31 | self.js.dom.time.innerHTML = t 32 | time.sleep(.1) 33 | 34 | @app.get('/', response_class=HTMLResponse) 35 | async def index_page(): 36 | App.main() 37 | html = """ 38 |

WHEN

39 | 40 | 41 | """ 42 | return App.render(html) 43 | 44 | if __name__ == "__main__": 45 | uvicorn.run(app, host="0.0.0.0", port=8000) 46 | -------------------------------------------------------------------------------- /examples/Flask/clock.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, request 2 | app = Flask(__name__) 3 | 4 | import jyserver.Flask as jsf 5 | import time 6 | @jsf.use(app) 7 | class App: 8 | def reset(self): 9 | self.start0 = time.time() 10 | self.js.dom.time.innerHTML = "{:.1f}".format(0) 11 | def stop(self): 12 | self.running = False 13 | self.js.dom.b2.innerHTML = "Restart" 14 | self.js.dom.b2.onclick = self.restart 15 | def restart(self): 16 | self.running = True 17 | self.js.dom.b2.innerHTML = "Pause" 18 | self.js.dom.b2.onclick = self.stop 19 | 20 | @jsf.task 21 | def main(self): 22 | self.running = True 23 | self.start0 = time.time() 24 | for i in range(1000): 25 | if self.running: 26 | t = "{:.1f}".format(time.time() - self.start0) 27 | self.js.dom.time.innerHTML = t 28 | time.sleep(.1) 29 | 30 | @app.route('/') 31 | def index_page(name=None): 32 | App.main() 33 | return App.render(render_template('clock.html')) -------------------------------------------------------------------------------- /examples/Flask/simple.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, request 2 | app = Flask(__name__) 3 | 4 | import jyserver.Flask as jsf 5 | @jsf.use(app) 6 | class App: 7 | def __init__(self): 8 | self.count = 0 9 | def increment(self): 10 | self.count += 1 11 | self.js.document.getElementById("count").innerHTML = self.count 12 | 13 | @app.route('/') 14 | def index_page(name=None): 15 | return App.render(render_template('flask-simple.html')) -------------------------------------------------------------------------------- /examples/Flask/templates/clock.html: -------------------------------------------------------------------------------- 1 |

WHEN

2 | 3 | -------------------------------------------------------------------------------- /examples/Flask/templates/flask-simple.html: -------------------------------------------------------------------------------- 1 |

0

2 | -------------------------------------------------------------------------------- /examples/Server/clock/clock.py: -------------------------------------------------------------------------------- 1 | #!/usr/env/bin python3 2 | 3 | from jyserver.Server import Server, Client 4 | import time 5 | 6 | class App(Client): 7 | def __init__(self): 8 | self.html = """ 9 |

WHEN

10 | 11 | 12 | """ 13 | self.running = True 14 | 15 | def reset(self): 16 | self.start0 = time.time() 17 | self.js.dom.time.innerHTML = "{:.1f}".format(0) 18 | 19 | def start(self): 20 | self.running = True 21 | 22 | def stop(self): 23 | self.running = False 24 | self.js.dom.b2.innerHTML = "Restart" 25 | self.js.dom.b2.onclick = self.restart 26 | 27 | def restart(self): 28 | self.running = True 29 | self.js.dom.b2.innerHTML = "Pause" 30 | self.js.dom.b2.onclick = self.stop 31 | 32 | def main(self): 33 | self.start0 = time.time() 34 | while True: 35 | if self.running: 36 | self.js.dom.time.innerHTML = "{:.1f}".format(time.time() - self.start0) 37 | time.sleep(0.1) 38 | 39 | httpd = Server(App) 40 | print("serving at port", httpd.port) 41 | httpd.start(cookies=False) -------------------------------------------------------------------------------- /examples/Server/increment/index.html: -------------------------------------------------------------------------------- 1 | 2 |

0

3 | 4 | -------------------------------------------------------------------------------- /examples/Server/increment/server.py: -------------------------------------------------------------------------------- 1 | from jyserver.Server import Client, Server 2 | class App(Client): 3 | def __init__(self): 4 | self.count = 0 5 | def increment(self): 6 | self.count += 1 7 | self.js.document.getElementById("count").innerHTML = self.count 8 | httpd = Server(App) 9 | print("serving at port", httpd.port) 10 | httpd.start() -------------------------------------------------------------------------------- /examples/Server/steps/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 |

Wow!

11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/Server/steps/srv.py: -------------------------------------------------------------------------------- 1 | #!/usr/env/bin python3 2 | 3 | from jyserver.Server import Server, Client 4 | import time 5 | 6 | class App(Client): 7 | def test1(self, n1, n2): 8 | return n1+n2 9 | 10 | def button1(self): 11 | # js.document.getElementById("txt").innerHTML = "BUTTON" 12 | self.js.dom.txt.innerHTML = "BUTTON" 13 | print("BUTTON1") 14 | 15 | def other(self, options): 16 | return "Other page: " + str(options) 17 | 18 | httpd = Server(App) 19 | print("serving at port", httpd.port) 20 | httpd.start(wait=False) 21 | 22 | js = httpd.js() 23 | js.val("info", js.document.getElementById("txt").innerHTML) 24 | for i in range(100): 25 | print("STEP", i) 26 | js.statevar = {"c":123,"d":(5,"x",3)} 27 | js.statevar.c = 99 28 | js["info"] = "test%d" % i 29 | print(js.statevar.c) 30 | n = js.square(i) 31 | print(i,n) 32 | time.sleep(1) -------------------------------------------------------------------------------- /jyserver/Bottle.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module for using jyserver in Bottle. This module provides two new 3 | decorators. 4 | 5 | Decorators 6 | ----------- 7 | 8 | * @use 9 | 10 | Link an application object to the Bottle app 11 | 12 | * @task 13 | 14 | Helper that wraps a function inside a separate thread so that 15 | it can execute concurrently. 16 | 17 | Example 18 | ------------- 19 | ```python 20 | from bottle import route, run 21 | import jyserver.Bottle as js 22 | import time 23 | 24 | @js.use 25 | class App(): 26 | def reset(self): 27 | self.start0 = time.time() 28 | 29 | @js.task 30 | def main(self): 31 | self.start0 = time.time() 32 | while True: 33 | t = "{:.1f}".format(time.time() - self.start0) 34 | self.js.dom.time.innerHTML = t 35 | time.sleep(0.1) 36 | 37 | @route('/') 38 | def index(): 39 | html = """ 40 |

WHEN

41 | 42 | """ 43 | App.main() 44 | return App.render(html) 45 | 46 | run(host='localhost', port=8080) 47 | ``` 48 | ''' 49 | 50 | from bottle import route, request 51 | 52 | import json 53 | import jyserver 54 | import threading 55 | 56 | def task(func): 57 | ''' 58 | Decorator wraps the function in a separate thread for concurrent 59 | execution. 60 | ''' 61 | def wrapper(*args): 62 | server_thread = threading.Thread(target=func, args=(args), daemon=True) 63 | server_thread.start() 64 | return wrapper 65 | 66 | def use(appClass): 67 | ''' 68 | Link a class to an app object. 69 | ''' 70 | global context 71 | context = jyserver.ClientContext(appClass) 72 | 73 | @route('/_process_srv0', method='POST') 74 | def process(): 75 | if request.method == 'POST': 76 | result = context.processCommand(request.json) 77 | if result is None: 78 | return '' 79 | return result 80 | return context -------------------------------------------------------------------------------- /jyserver/Django.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module for using jyserver in Django. This module provides two new 3 | decorators. 4 | 5 | Decorators 6 | ----------- 7 | 8 | * @use 9 | 10 | Link an application object to the Django app 11 | 12 | * @task 13 | 14 | Helper that wraps a function inside a separate thread so that 15 | it can execute concurrently. 16 | 17 | Example (assumes working setup) 18 | ------------- 19 | ```python 20 | from django.shortcuts import render 21 | import jyserver.Django as js 22 | import time 23 | 24 | @js.use 25 | class App(): 26 | def reset(self): 27 | self.start0 = time.time() 28 | self.js.dom.time.innerHTML = "{:.1f}".format(0) 29 | 30 | @js.task 31 | def main(self): 32 | self.start0 = time.time() 33 | while True: 34 | t = "{:.1f}".format(time.time() - self.start0) 35 | self.js.dom.time.innerHTML = t 36 | time.sleep(0.1) 37 | 38 | def hello_world(request): 39 | App.main() 40 | return App.render(render(request, 'hello_world.html', {})) 41 | ``` 42 | 43 | In `urls.py` add this path: 44 | 45 | ```python 46 | from jyserver.Django import process 47 | ... 48 | url(r'^_process_srv0$', process, name='process'), 49 | ``` 50 | ''' 51 | 52 | from django.shortcuts import render 53 | from django.http import HttpResponse 54 | from django.views.decorators.csrf import csrf_exempt 55 | 56 | import json 57 | import jyserver 58 | import threading 59 | 60 | def task(func): 61 | ''' 62 | Decorator wraps the function in a separate thread for concurrent 63 | execution. 64 | ''' 65 | def wrapper(*args): 66 | server_thread = threading.Thread(target=func, args=args, daemon=True) 67 | server_thread.start() 68 | return wrapper 69 | 70 | @csrf_exempt 71 | def process(request): 72 | ''' 73 | Used to process browser requests. Must be added to your `urls.py` 74 | to process `/_process_srv0` as in: 75 | ``` 76 | url(r'^_process_srv0$', process, name='process'), 77 | ``` 78 | ''' 79 | if request.method == 'POST': 80 | req = json.loads(request.body) 81 | result = context.processCommand(req) 82 | if result is None: 83 | return HttpResponse('') 84 | return HttpResponse(result) 85 | else: 86 | return HttpResponse("GET reqeust not allowed") 87 | 88 | def use(appClass): 89 | ''' 90 | Link a class to an app object. Pass `app` object. 91 | ''' 92 | global context 93 | context = jyserver.ClientContext(appClass) 94 | context.render = context.render_django 95 | return context 96 | -------------------------------------------------------------------------------- /jyserver/FastAPI.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module for using jyserver in FastAPI. This module provides two new 3 | decorators. 4 | 5 | Decorators 6 | ----------- 7 | 8 | * @use 9 | 10 | Link an application object to the Flask app 11 | 12 | * @task 13 | 14 | Helper that wraps a function inside a separate thread so that 15 | it can execute concurrently. 16 | 17 | Example 18 | ------------- 19 | ```python 20 | import jyserver.FastAPI as js 21 | import time 22 | 23 | from fastapi import FastAPI 24 | from fastapi.responses import HTMLResponse 25 | 26 | app = FastAPI(__name__) 27 | 28 | @js.use(app) 29 | class App(): 30 | def reset(self): 31 | self.start0 = time.time() 32 | self.js.dom.time.innerHTML = "{:.1f}".format(0) 33 | 34 | @js.task 35 | def main(self): 36 | self.start0 = time.time() 37 | while True: 38 | t = "{:.1f}".format(time.time() - self.start0) 39 | self.js.dom.time.innerHTML = t 40 | time.sleep(0.1) 41 | 42 | @app.get('/', response_class=HTMLResponse) 43 | async def index_page(): 44 | App.main() 45 | html = """ 46 |

TIME

47 | 48 | """ 49 | return App.render(html) 50 | ``` 51 | ''' 52 | 53 | from fastapi import FastAPI, Request, Response 54 | from fastapi.responses import HTMLResponse 55 | from pydantic import BaseModel 56 | 57 | import jyserver 58 | import threading 59 | 60 | def task(func): 61 | ''' 62 | Decorator wraps the function in a separate thread for concurrent 63 | execution. 64 | ''' 65 | def wrapper(*args): 66 | server_thread = threading.Thread(target=func, args=args, daemon=True) 67 | server_thread.start() 68 | return wrapper 69 | 70 | def use(myapp): 71 | ''' 72 | Link a class to an app object. Pass `app` object. 73 | ''' 74 | def decorator(appClass): 75 | global context 76 | context = jyserver.ClientContext(appClass) 77 | 78 | @myapp.post('/_process_srv0') 79 | async def process(item: Request): 80 | req = await item.json() 81 | result = context.processCommand(req) 82 | if result is None: result = '' 83 | return Response(content=result, media_type="text/plain") 84 | return context 85 | 86 | return decorator -------------------------------------------------------------------------------- /jyserver/Flask.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module for using jyserver in Flask. This module provides two new 3 | decorators. 4 | 5 | Decorators 6 | ----------- 7 | 8 | * @use 9 | 10 | Link an application object to the Flask app 11 | 12 | * @task 13 | 14 | Helper that wraps a function inside a separate thread so that 15 | it can execute concurrently. 16 | 17 | Example 18 | ------------- 19 | ```html 20 |

TIME

21 | 22 | ``` 23 | 24 | ```python 25 | import jyserver.Flask as js 26 | import time 27 | from flask import Flask, render_template, request 28 | 29 | app = Flask(__name__) 30 | 31 | @js.use(app) 32 | class App(): 33 | def reset(self): 34 | self.start0 = time.time() 35 | self.js.dom.time.innerHTML = "{:.1f}".format(0) 36 | 37 | @js.task 38 | def main(self): 39 | self.start0 = time.time() 40 | while True: 41 | t = "{:.1f}".format(time.time() - self.start0) 42 | self.js.dom.time.innerHTML = t 43 | time.sleep(0.1) 44 | 45 | @app.route('/') 46 | def index_page(name=None): 47 | App.main() 48 | return App.render(render_template('flask-simple.html') 49 | ''' 50 | 51 | from flask import Flask, request 52 | import json 53 | import jyserver 54 | import threading 55 | 56 | def task(func): 57 | ''' 58 | Decorator wraps the function in a separate thread for concurrent 59 | execution. 60 | ''' 61 | def wrapper(*args): 62 | server_thread = threading.Thread(target=func, args=args, daemon=True) 63 | server_thread.start() 64 | return wrapper 65 | 66 | def use(flaskapp): 67 | ''' 68 | Link a class to an app object. Pass Flask's `app` object. 69 | ''' 70 | def decorator(appClass): 71 | global context 72 | context = jyserver.ClientContext(appClass) 73 | 74 | @flaskapp.route('/_process_srv0', methods=['GET', 'POST']) 75 | def process(): 76 | if request.method == 'POST': 77 | req = json.loads(request.data) 78 | result = context.processCommand(req) 79 | if result is None: 80 | return '' 81 | return result 82 | else: 83 | return "GET request not allowed" 84 | return context 85 | 86 | return decorator -------------------------------------------------------------------------------- /jyserver/Server.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module for using jyserver standalone. This module uses the built-in 3 | http.server module. It serves as a framework for integration into 4 | other servers. 5 | 6 | Example 7 | ------------- 8 | ```python 9 | from jserver import Client, Server 10 | class App(Client): 11 | def __init__(self): 12 | self.html = """ 13 |

TIME

14 | 16 | """ 17 | 18 | def reset(self): 19 | self.start0 = time.time() 20 | self.js.dom.time.innerHTML = "{:.1f}".format(0) 21 | 22 | def main(self): 23 | self.start0 = time.time() 24 | while True: 25 | t = "{:.1f}".format(time.time() - self.start0) 26 | self.js.dom.time.innerHTML = t 27 | time.sleep(0.1) 28 | 29 | httpd = Server(App) 30 | print("serving at port", httpd.port) 31 | httpd.start() 32 | ``` 33 | ''' 34 | 35 | from socketserver import ThreadingTCPServer 36 | from http.server import SimpleHTTPRequestHandler 37 | from http.cookies import SimpleCookie 38 | from urllib.parse import urlparse, parse_qsl, unquote 39 | 40 | from jyserver import ClientContext, HtmlPage 41 | 42 | import json 43 | import threading 44 | import queue 45 | import os 46 | import copy 47 | import re 48 | import time 49 | import uuid 50 | 51 | 52 | class Client: 53 | ''' 54 | Client class contains all methods and code that is executed on the server 55 | and browser. Users of this library should inherit this class and implement 56 | methods. There are three types of methods: 57 | 58 | Attributes 59 | ------------ 60 | home 61 | Optional filename to send when "/" is requested 62 | html 63 | Optional HTML to send when "/" is requested. If neither 64 | `home` nor `html` are set, then it will send "index.html" 65 | js 66 | JS object for constructing and executing Javascript. 67 | 68 | Methods 69 | ----------- 70 | 71 | h(file, html) 72 | Return appropriate HTML for the active page. Can only 73 | be called once per page. Must be called if implementing 74 | custom pages. 75 | 76 | Optional Methods 77 | ------------ 78 | * main(self) 79 | 80 | If this is implemented, then the server will begin execution of this 81 | function immediately. The server will terminate when this function 82 | terminates. 83 | 84 | * index(self) 85 | 86 | If `index` is defined, it will execute this function. The function 87 | is responsible for returning the HTML with the h() method. 88 | 89 | * page(self) 90 | 91 | When the browser clicks on a link (or issues a GET) a method with the 92 | name of the page is executed. For example, clicking on link "http:/pg1" 93 | will cause a method named "pg1" to be executed. 94 | 95 | * func(self) 96 | 97 | When the browser executes a "server" command, the server runs a method 98 | with the same name. For example, if the browser runs the Javascript 99 | code: 100 | 101 | server.addnum(15, 65) 102 | 103 | then this method will be called: 104 | 105 | def func(self, 15, 65) 106 | ''' 107 | def __init__(self): 108 | self.js = None 109 | self._state = None 110 | 111 | def h(self, html=None, file=None): 112 | ''' 113 | Convert text to html and wrap with script code. Return the HTML as a 114 | byte string. Must be called if implementing a custom page 115 | such as `index`. 116 | ''' 117 | return self._state.htmlsend(html, file) 118 | 119 | class Server(ThreadingTCPServer): 120 | ''' 121 | Server implements the web server, waits for connections and processes 122 | commands. Each browser request is handled in its own thread and so requests 123 | are asynchronous. The server starts listening when the "start()" method is 124 | called. 125 | 126 | Methods 127 | ------------ 128 | start(wait, cookies) 129 | ''' 130 | 131 | PORT = 8080 132 | allow_reuse_address = True 133 | 134 | def __init__(self, appClass, port=PORT, ip=None, verbose=False): 135 | ''' 136 | Parameters 137 | ------------- 138 | appClass 139 | Class that inherits Client. Note that this is the 140 | class name and not an instance. 141 | port 142 | Port to listen to (default is PORT) 143 | ip 144 | IP address to bind (default is all) 145 | ''' 146 | self.verbose = verbose 147 | # Instantiate objects of this class; must inherit from Client 148 | self.appClass = appClass 149 | self.contextMap = {} 150 | # The port number 151 | self.port = port 152 | if ip is None: 153 | ip = '127.0.0.1' 154 | # Create the server object. Must call start() to begin listening. 155 | super(Server, self).__init__((ip, port), Handler) 156 | 157 | # def getContext(self): 158 | # return self._getContextForPage('SINGLE') 159 | 160 | def js(self): 161 | ''' 162 | If you are implementing a single application without a "main" 163 | function, you can call this to retrieve the JS object and set 164 | up for single instance execution. 165 | ''' 166 | return self._getContextForPage('SINGLE', True).getJS() 167 | 168 | def _getContextForPage(self, uid, create = False): 169 | c = ClientContext._getContextForPage(uid, self.appClass, create=create, verbose=self.verbose) 170 | return c 171 | 172 | def stop(self): 173 | # self._BaseServer__shutdown_request = True 174 | self._runmode = False 175 | # self.shutdown() 176 | 177 | def _runServer(self): 178 | ''' 179 | Begin running the server until terminated. 180 | ''' 181 | self._runmode = True 182 | while self._runmode: 183 | self.handle_request() 184 | # self.serve_forever() 185 | self.log_message("SERVER TERMINATED") 186 | 187 | def start(self, wait=True, cookies=True): 188 | ''' 189 | Start listening to the port and processing requests. 190 | 191 | Parameters 192 | ------------ 193 | wait 194 | Start listening and wait for server to terminate. If this 195 | is false, start server on new thread and continue execution. 196 | cookies 197 | If True, try to use cookies to keep track of sessions. This 198 | enables the browser to open multiple windows that all share 199 | the same Client object. If False, then cookies are disabled 200 | and each tab will be it's own session. 201 | ''' 202 | self.useCookies = cookies 203 | if wait or hasattr(self.appClass, "main"): 204 | self._runServer() 205 | else: 206 | server_thread = threading.Thread(target=self._runServer, daemon=True) 207 | server_thread.start() 208 | 209 | def log_message(self, format, *args): 210 | if self.verbose: 211 | print(format % args) 212 | def log_error(self, format, *args): 213 | print(format % args) 214 | 215 | class Handler(SimpleHTTPRequestHandler): 216 | ''' 217 | Handler is created for each request by the Server. This class 218 | handles the page requests and delegates tasks. 219 | ''' 220 | 221 | def getContext(self): 222 | return self.server._getContextForPage(self.uid) 223 | 224 | def reply(self, data, num=200): 225 | ''' 226 | Reply to the client with the given status code. If data is given as a string 227 | it will be encoded at utf8. Cookies are sent if they are used. 228 | ''' 229 | self.send_response(num) 230 | if self.server.useCookies: 231 | self.send_header( 232 | "Set-Cookie", self.cookies.output(header='', sep='')) 233 | self.end_headers() 234 | 235 | if data is None: 236 | return 237 | 238 | if isinstance(data, str): 239 | data = data.encode("utf8") 240 | 241 | try: 242 | self.wfile.write(data) 243 | self.log_message("REPLY DONE") 244 | except Exception as ex: 245 | traceback.print_exc() 246 | self.server.log_error("Error sending: %s", str(ex)) 247 | 248 | def replyFile(self, path, num=200): 249 | ''' 250 | Reply to client with given file. 251 | ''' 252 | with open(path, "rb") as f: 253 | block = f.read() 254 | result = HtmlPage(block).html(self.uid) 255 | self.reply(result) 256 | 257 | def processCookies(self): 258 | ''' 259 | Read in cookies and extract the session id. 260 | ''' 261 | if self.server.useCookies: 262 | self.cookies = SimpleCookie(self.headers.get('Cookie')) 263 | if "UID" in self.cookies: 264 | self.uid = self.cookies["UID"] 265 | else: 266 | self.uid = None 267 | 268 | def do_GET(self): 269 | ''' 270 | Called by parent to process GET requests. Forwards requests to do_PAGE. 271 | ''' 272 | if not self.server._runmode: return 273 | self.processCookies() 274 | qry = urlparse(self.path) 275 | req = dict(parse_qsl(qry.query)) 276 | self.server.log_message("GET %s %s", qry, req) 277 | if "session" in req: 278 | pageid = req["session"] 279 | if pageid in HtmlPage.pageMap: 280 | self.uid = HtmlPage.pageMap[pageid] 281 | else: 282 | self.uid = None 283 | else: 284 | self.uid = None 285 | # self.setNewUID() 286 | 287 | if qry.path == "/": 288 | # result = self.server._getHome(self.uid) 289 | c = self.getContext() 290 | result = c.showHome() 291 | if callable(result): 292 | self.log_message("HOME CALL %s", result) 293 | c.showPage(self, result, qry) 294 | else: 295 | self.log_message("HOME SEND %s", result) 296 | self.reply(result) 297 | elif qry.path == "/appscript.js": 298 | self.reply(JSCRIPT) 299 | else: 300 | self.do_PAGE(qry) 301 | 302 | def do_POST(self): 303 | ''' 304 | Called by parent to process POST requests. Handles the built-in 305 | /state and /run requests and forwards all others to do_PAGE. 306 | ''' 307 | if not self.server._runmode: return 308 | self.processCookies() 309 | l = int(self.headers["Content-length"]) 310 | data = self.rfile.read(l) 311 | self.log_message("HTTP POST %s", data) 312 | if self.path == "/_process_srv0": 313 | self.log_message("PROCESS %s", data) 314 | req = json.loads(data) 315 | if "session" in req: 316 | pageid = req["session"] 317 | if pageid in HtmlPage.pageMap: 318 | self.uid = HtmlPage.pageMap[pageid] 319 | else: 320 | self.uid = None 321 | else: 322 | self.uid = None 323 | c = self.getContext() 324 | results = c.processCommand(req) 325 | self.reply(results) 326 | else: 327 | self.do_PAGE(data) 328 | 329 | def do_PAGE(self, qry): 330 | ''' 331 | Process page requests except /state and /run. 332 | ''' 333 | self.log_message("PAGE %s", qry) 334 | if os.path.exists(qry.path[1:]): 335 | # try to send a file with the given name if it exists. 336 | self.replyFile(qry.path[1:]) 337 | else: 338 | # otherwise, pass on the request to the Client object. It will 339 | # execute a method with the same name if it exists. 340 | c = self.getContext() 341 | c.showPage(self, qry.path, qry) 342 | 343 | def log_message(self, format, *args): 344 | if self.server.verbose: 345 | print(format % args) 346 | 347 | -------------------------------------------------------------------------------- /jyserver/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Jyserver is a framework for simplifying the creation of font ends for apps and 3 | kiosks by providing real-time access to the browser's DOM and Javascript from 4 | the server using Python syntax. It also provides access to the Python code from 5 | the browser's Javascript. It can be used stand-alone or with other 6 | frameworks such as Flask, Django, etc. See examples folder. 7 | 8 | Source: https://github.com/ftrias/jyserver 9 | 10 | Example using Bottle 11 | ------------------------------- 12 | ``` 13 | from bottle import route, run 14 | import jyserver.Bottle as js 15 | import time 16 | 17 | @js.use 18 | class App(): 19 | def reset(self): 20 | self.start0 = time.time() 21 | 22 | @js.task 23 | def main(self): 24 | self.start0 = time.time() 25 | while True: 26 | t = "{:.1f}".format(time.time() - self.start0) 27 | self.js.dom.time.innerHTML = t 28 | time.sleep(0.1) 29 | 30 | @route('/') 31 | def index(): 32 | html = """ 33 |

WHEN

34 | 35 | """ 36 | App.main() 37 | return App.render(html) 38 | 39 | run(host='localhost', port=8080) 40 | ``` 41 | ''' 42 | 43 | from inspect import signature 44 | import ctypes 45 | import traceback 46 | 47 | import json 48 | import threading 49 | import queue 50 | import os 51 | import copy 52 | import re 53 | import time 54 | import uuid 55 | 56 | from . import jscript 57 | 58 | class AppParent: 59 | def __getitem__(self, k): 60 | return self.js.dom.__getattr__(k).innerHTML 61 | 62 | def __setitem__(self, k, v): 63 | self.js.dom.__getattr__(k).innerHTML = v 64 | 65 | def __getattr__(self, k): 66 | return self.js.dom.__getattr__(k) 67 | 68 | class ClientContext: 69 | contextMap = {} 70 | taskTimeout = 5 71 | 72 | def __init__(self, cls, uid=None, verbose=False): 73 | self.appClass = cls 74 | self.obj = cls() 75 | cls.__getitem__ = AppParent.__getitem__ 76 | cls.__setitem__ = AppParent.__setitem__ 77 | cls.__getattr__ = AppParent.__getattr__ 78 | self.queries = {} 79 | self.lock = threading.Lock() 80 | self.fxn = {} 81 | self.verbose = verbose 82 | self.tasks = queue.Queue() 83 | self.uid = uid 84 | self._error = None 85 | self._signal = None 86 | self.obj.js = JSroot(self) 87 | self.singleThread = False 88 | 89 | def render(self, html): 90 | ''' 91 | Add Javascript to the given html that will enable use of this 92 | module. If using Django, this gets reassigned to `render_django()`. 93 | ''' 94 | page = HtmlPage(html=html) 95 | html = page.html(self.uid) 96 | return html 97 | 98 | def render_django(self, inp): 99 | ''' 100 | Version of `render()` for use with Django. 101 | ''' 102 | # for Django support 103 | page = HtmlPage(html=inp.content) 104 | inp.content = page.html(self.uid) 105 | return inp 106 | 107 | def htmlsend(self, html=None, file=None): 108 | page = HtmlPage(html=html, file=file) 109 | html = page.html(self.uid) 110 | self._handler.reply(html) 111 | self.log_message("SET SIGNAL %s", id(self._signal)) 112 | self._signal.set() 113 | return page 114 | 115 | def hasMethod(self, name): 116 | return hasattr(self.obj, name) 117 | 118 | def callMethod(self, name, args=None): 119 | if hasattr(self.obj, name): 120 | f = getattr(self.obj, name) 121 | if args is None: 122 | f() 123 | else: 124 | f(*args) 125 | else: 126 | raise ValueError("Method not found: " + name) 127 | 128 | def __getattr__(self, attr): 129 | ''' 130 | Unhandled calls to the context get routed to the app 131 | object. 132 | ''' 133 | return self.obj.__getattribute__(attr) 134 | 135 | def getJS(self): 136 | ''' 137 | Return the JS object tied to this context. Use the return value to 138 | create JS statements. 139 | ''' 140 | return self.obj.js 141 | 142 | @classmethod 143 | def _getContextForPage(self, uid, appClass, create = False, verbose = False): 144 | ''' 145 | Retrieve the Client instance for a given session id. If `create` is 146 | True, then if the app is not found a new one will be created. Otherwise 147 | if the app is not found, return None. 148 | ''' 149 | if uid and not isinstance(uid, str): 150 | # if uid is a cookie, get it's value 151 | uid = uid.value 152 | 153 | # if we're not using cookies, direct links have uid of None 154 | if uid is None: 155 | # get first key 156 | if len(self.contextMap) > 0: 157 | uid = list(self.contextMap.items())[0][0] 158 | else: 159 | uid = None 160 | 161 | # existing app? return it 162 | if uid in self.contextMap: 163 | return self.contextMap[uid] 164 | else: 165 | # this is a new session or invalid session 166 | # assign it a new id 167 | # Instantiate Client, call initialize and save it. 168 | context = ClientContext(appClass, uid, verbose=verbose) 169 | self.contextMap[uid] = context 170 | context.log_message("NEW CONTEXT %s ID=%s", uid, id(self)) 171 | # If there is a "main" function, then start a new thread to run it. 172 | # _mainRun will run main and terminate the server after main returns. 173 | context.mainRun() 174 | return context 175 | 176 | raise ValueError("Invalid or empty seession id: %s" % uid) 177 | 178 | def processCommand(self, req): 179 | ''' 180 | Process the /_process_srv0 requests. All client requests are directed to this 181 | URL and the framework is responsible for calling this function to process 182 | them. 183 | ''' 184 | pageid = req["session"] 185 | if pageid in HtmlPage.pageMap: 186 | self.uid = HtmlPage.pageMap[pageid] 187 | else: 188 | self.log_message("Invalid page id session %s", pageid) 189 | return 'Invalid pageid session: ' + pageid 190 | # raise RuntimeError("Invalid pageid session: " + pageid) 191 | 192 | HtmlPage.pageActive[pageid] = time.time() 193 | 194 | task = req["task"] 195 | self.log_message("RECEIVE TASK %s %s %s", task, self.uid, pageid) 196 | if task == "state": 197 | # The browser is replying to a request for data. First, find 198 | # the corresponding Queue for our request. 199 | q = self.getQuery(req['query']) 200 | # Add the results to the Queue, the code making the request is 201 | # currently waiting with a get(). This will cause that code 202 | # to wake up and process the results. 203 | q.put(req) 204 | # confirm to the server that we have processed this. 205 | return str(req) 206 | elif task == "run": 207 | # here, the browser is requesting we execute a statement and 208 | # return the results. 209 | result = self.run(req['function'], req['args']) 210 | return result 211 | elif task == "get": 212 | # here, the browser is requesting we execute a statement and 213 | # return the results. 214 | result = self.get(req['expression']) 215 | return result 216 | elif task == "set": 217 | # here, the browser is requesting we execute a statement and 218 | # return the results. 219 | result = self.set(req['property'], req['value']) 220 | return '' 221 | elif task == "async": 222 | # here, the browser is requesting we execute a statement and 223 | # return the results. 224 | result = self.run(req['function'], req['args'], block=False) 225 | return result 226 | elif task == "next": 227 | # the Browser is requesting we evaluate an expression and 228 | # return the results. 229 | script = self.getNextTask() 230 | self.log_message("NEXT TASK REQUESTED IS JS %s", script) 231 | return script 232 | elif task == "error": 233 | # return '' 234 | self._error = RuntimeError(req['error'] + ": " + req["expr"]) 235 | return '' 236 | elif task == "unload": 237 | self.addEndTask() 238 | HtmlPage.expire(pageid) 239 | # HtmlPage.raiseException(pageid, RuntimeError("unload")) 240 | self.log_message("UNLOAD %s", pageid) 241 | return '' 242 | 243 | def getQuery(self, query): 244 | ''' 245 | Each query sent to the browser is assigned to it's own Queue to wait for 246 | a response. This function returns the Queue for the given session id and query. 247 | ''' 248 | return self.queries[query] 249 | 250 | def addQuery(self): 251 | ''' 252 | Set query is assigned to it's own Queue to wait for 253 | a response. This function returns the Queue for the given session id and query. 254 | ''' 255 | q = queue.Queue() 256 | self.queries[id(q)] = q 257 | return id(q), q 258 | 259 | def delQuery(self, query): 260 | ''' 261 | Delete query is assigned to it's own Queue to wait for 262 | a response. This function returns the Queue for the given session id and query. 263 | ''' 264 | return self.queries[query] 265 | 266 | def addTask(self, stmt): 267 | ''' 268 | Add a task to the queue. If the queue is too long (5 in this case) 269 | the browser is too slow for the speed at which we are sending commands. 270 | In that case, wait for up to one second before sending the command. 271 | Perhaps the wait time and queue length should be configurable because they 272 | affect responsiveness. 273 | ''' 274 | for _ in range(10): 275 | if self.tasks.qsize() < 5: 276 | self.tasks.put(stmt) 277 | self.log_message("ADD TASK %s ON %d", stmt, id(self.tasks)) 278 | return 279 | time.sleep(0.1) 280 | self._error = TimeoutError("Timeout (deadlock?) inserting task: " + stmt) 281 | 282 | 283 | def run(self, function, args, block=True): 284 | ''' 285 | Called by the framework to execute a method. This function will look for a method 286 | with the given name. If it is found, it will execute it. If it is not found it 287 | will return a string saying so. If there is an error during execution it will 288 | return a string with the error message. 289 | ''' 290 | self.log_message("RUN %s %s", function, args) 291 | if block: 292 | if not self.lock.acquire(blocking = False): 293 | raise RuntimeError("App is active and would block") 294 | 295 | try: 296 | if function == "_callfxn": 297 | # first argument is the function name 298 | # subsequent args are optional 299 | fxn = args.pop(0) 300 | f = self.fxn[fxn] 301 | elif callable(function): 302 | f = function 303 | elif hasattr(self.obj, function): 304 | f = getattr(self.obj, function) 305 | else: 306 | f = None 307 | 308 | if f: 309 | try: 310 | result = f(*args) 311 | ret = json.dumps({"value":JSroot._v(result)}) 312 | except Exception as ex: 313 | s = "%s: %s" % (type(ex).__name__, str(ex)) 314 | if self.verbose: traceback.print_exc() 315 | self.log_message("Exception passed to browser: %s", s) 316 | ret = json.dumps({"error":s}) 317 | else: 318 | result = "Unsupported: " + function + "(" + str(args) + ")" 319 | ret = json.dumps({"error":str(result)}) 320 | self.log_message("RUN RESULT %s", ret) 321 | return ret 322 | finally: 323 | if block: 324 | self.lock.release() 325 | 326 | def get(self, expr): 327 | ''' 328 | Called by the framework to execute a method. This function will look for a method 329 | with the given name. If it is found, it will execute it. If it is not found it 330 | will return a string saying so. If there is an error during execution it will 331 | return a string with the error message. 332 | ''' 333 | self.log_message("GET EXPR %s", expr) 334 | if not self.lock.acquire(blocking = False): 335 | raise RuntimeError("App is active and would block") 336 | 337 | try: 338 | if hasattr(self.obj, expr): 339 | value = getattr(self.obj, expr) 340 | if callable(value): 341 | value = "(function(...args) { return handleApp('%s', args) })" % expr 342 | return json.dumps({"type":"expression", "expression":value}) 343 | else: 344 | return json.dumps({"type":"value", "value":value}) 345 | return None 346 | finally: 347 | self.lock.release() 348 | 349 | def set(self, expr, value): 350 | ''' 351 | Called by the framework to set a propery. 352 | ''' 353 | self.log_message("SET EXPR %s = %s", expr, value) 354 | self.obj.__setattr__(expr, value) 355 | return value 356 | 357 | def getNextTask(self): 358 | ''' 359 | Wait for new tasks and return the next one. It will wait for 1 second and if 360 | there are no tasks return None. 361 | ''' 362 | try: 363 | self.log_message("TASKS WAITING %d ON %d", self.tasks.qsize(), id(self.tasks)) 364 | return self.tasks.get(timeout=self.taskTimeout) 365 | except queue.Empty: 366 | return None 367 | 368 | def addEndTask(self): 369 | ''' 370 | Add a None task to end the queue. 371 | ''' 372 | self.log_message("TASKS END %d ON %d", self.tasks.qsize(), id(self.tasks)) 373 | self.tasks.put(None) 374 | 375 | def mainRun(self): 376 | ''' 377 | If there is a method called `main` in the client app, then run it in its own 378 | thread. 379 | ''' 380 | if hasattr(self.obj, "main"): 381 | server_thread = threading.Thread( 382 | target=self.mainRunThread, daemon=True) 383 | server_thread.start() 384 | 385 | def mainRunThread(self): 386 | ''' 387 | Run the main function. When the function ends, terminate the server. 388 | ''' 389 | try: 390 | self.obj.main() 391 | except Exception as ex: 392 | self.log_message("FATAL ERROR: %s", ex) 393 | 394 | def showPage(self, handler, path, query): 395 | ''' 396 | Called by framework to return a queried page. When the browser requests a web page 397 | (for example when a user clicks on a link), the path will get put in `path` and 398 | any paramters passed through GET or POST will get passed in `query`. This will 399 | look for a Client method with the same name as the page requested. If found, it will 400 | execute it and return the results. If not, it will return "not found", status 404. 401 | ''' 402 | if callable(path): 403 | f = path 404 | else: 405 | fxn = path[1:].replace('/', '_').replace('.', '_') 406 | if hasattr(self.obj, fxn): 407 | f = getattr(self.obj, fxn) 408 | elif path == "/favicon.ico": 409 | handler.reply("Not found %s" % path, 404) 410 | return 411 | else: 412 | raise RuntimeWarning("Page not found: " + path) 413 | # return "Not found", 404 414 | 415 | self._handler = handler 416 | self._signal = threading.Event() 417 | self.log_message("START PAGE %s %d", path, id(self._signal)) 418 | server_thread = threading.Thread(target=self.run_callable, 419 | args=(f, {"page": path, "query": query}), daemon=True) 420 | server_thread.start() 421 | self.log_message("WAIT ON SIGNAL %s %d", path, id(self._signal)) 422 | self._signal.wait() # set when HTML is sent 423 | self._signal = None 424 | 425 | def run_callable(self, f, args): 426 | ''' 427 | Execute a callable (function, etc) and catch any exceptions. This 428 | is called when running pages asynchonously. 429 | ''' 430 | params = signature(f).parameters 431 | try: 432 | if len(params) == 0: 433 | f() 434 | else: 435 | f(args) 436 | except Exception as ex: 437 | traceback.print_exc() 438 | self.log_message("Exception: %s" % str(ex)) 439 | 440 | def showHome(self): 441 | ''' 442 | Get the home page when "/" is queried and inject the appropriate javascript 443 | code. Returns a byte string suitable for replying back to the browser. 444 | ''' 445 | if hasattr(self.obj, "html"): 446 | block = self.obj.html.encode("utf8") 447 | page = HtmlPage(block) 448 | self.activePage = page.pageid 449 | return page.html(self.uid) 450 | elif hasattr(self.obj, "home"): 451 | path = self.obj.home 452 | elif os.path.exists("index.html"): 453 | path = "index.html" 454 | elif hasattr(self.obj, "index"): 455 | return self.obj.index 456 | else: 457 | raise ValueError("Could not find index or home") 458 | 459 | with open(path, "rb") as f: 460 | block = f.read() 461 | page = HtmlPage(block) 462 | self.activePage = page.pageid 463 | return page.html(self.uid) 464 | 465 | def log_message(self, format, *args): 466 | if self.verbose: 467 | print(format % args) 468 | 469 | def log_error(self, format, *args): 470 | print(format % args) 471 | 472 | class HtmlPage: 473 | # Patterns for matching HTML to figure out where to inject the javascript code 474 | _pscript = re.compile( 475 | b'\\', re.IGNORECASE), 478 | re.compile(b'\\<\\/head\\>', re.IGNORECASE), 479 | re.compile(b'\\', re.IGNORECASE), 480 | re.compile(b'\\', re.IGNORECASE)] 481 | 482 | pageMap = {} 483 | pageActive = {} 484 | # pageThread = {} 485 | 486 | def __init__(self, html=None, file=None): 487 | if file: 488 | with open(file, "rb") as f: 489 | self.result = f.read() 490 | elif html: 491 | if isinstance(html, bytes): 492 | self.result = html 493 | else: 494 | self.result = html.encode("utf8") 495 | else: 496 | self.result = None 497 | self.pageid = uuid.uuid1().hex 498 | 499 | def alive(self): 500 | ''' 501 | See if the current page is in the active page list and has not been 502 | expired. 503 | ''' 504 | return self.pageid in self.pageActive 505 | 506 | @classmethod 507 | def expire(cls, item=None): 508 | ''' 509 | Expire objects in the page cache. 510 | ''' 511 | if item: 512 | del cls.pageActive[item] 513 | del cls.pageMap[item] 514 | 515 | old = time.time() - 5 516 | remove = [] 517 | for k,v in cls.pageActive.items(): 518 | if v < old: 519 | remove.append(k) 520 | for k in remove: 521 | del cls.pageActive[k] 522 | del cls.pageMap[k] 523 | 524 | def html(self, uid): 525 | ''' 526 | Once the page has been loaded, this will return the appropriate 527 | HTML for the uid given. 528 | ''' 529 | return self.insertJS(uid, self.result) 530 | 531 | def insertJS(self, uid, html): 532 | ''' 533 | Insert the Javascript library into HTML. The strategy is that it will look for patterns 534 | to figure out where to insert. If "" + html[sx:] 549 | for i, p in enumerate(self._plist): 550 | m = p.search(html) 551 | if m: 552 | sx, ex = m.span() 553 | if i == 0: 554 | return html[:sx] + U + jscript.JSCRIPT + html[ex:] 555 | elif i == 1: 556 | return html[:sx] + b"" + html[sx:] 557 | elif i == 2: 558 | return html[:sx] + b"" + html[sx:] 559 | else: 560 | return html[:sx] + b"" + html 561 | return b"" + html 562 | 563 | class JSchain: 564 | ''' 565 | JSchain keeps track of the dynamically generated Javascript. It 566 | tracks names, data item accesses and function calls. JSchain 567 | is usually not used directly, but accessed through the JS class. 568 | 569 | Attributes 570 | ----------- 571 | state 572 | A JSstate instance. This instance should be the same 573 | for all call of the same session. 574 | 575 | Notes 576 | ----------- 577 | There is a special name called `dom` which is shorthand for 578 | lookups. For example, 579 | 580 | js.dom.button1.innerHTML 581 | 582 | Becomes 583 | 584 | js.document.getElementById("button1").innerHTML 585 | 586 | Example 587 | -------------- 588 | ``` 589 | state = JSstate(server) 590 | js = JSchain(state) 591 | js.document.getElementById("txt").value 592 | ``` 593 | ''' 594 | 595 | def __init__(self, state): 596 | self.state = state 597 | self.chain = [] 598 | self.keep = True 599 | 600 | def _dup(self): 601 | ''' 602 | Duplicate this chain for processing. 603 | ''' 604 | js = JSchain(self.state) 605 | js.chain = self.chain.copy() # [x[:] for x in self.chain] 606 | return js 607 | 608 | def _add(self, attr, dot=True): 609 | ''' 610 | Add item to the chain. If `dot` is True, then a dot is added. If 611 | not, this is probably a function call and not dot should be added. 612 | ''' 613 | if not attr: 614 | # this happens when __setattr__ is called when the first 615 | # item of a JSchain is an assignment 616 | return self 617 | if dot and len(self.chain) > 0: 618 | self.chain.append(".") 619 | self.chain.append(attr) 620 | return self 621 | 622 | def _prepend(self, attr): 623 | ''' 624 | Add item to the start of the chain. 625 | ''' 626 | self.chain.insert(0, attr) 627 | return self 628 | 629 | def _last(self): 630 | ''' 631 | Last item on the chain. 632 | ''' 633 | return self.chain[-1] 634 | 635 | def __getattr__(self, attr): 636 | ''' 637 | Called to process items in a dot chain in Python syntax. For example, 638 | in a.b.c, this will get called for "b" and "c". 639 | ''' 640 | # __iter__ calls should be ignored 641 | if attr == "__iter__": 642 | return self 643 | return self.getdata(attr) 644 | 645 | def getdata(self, attr, adot=True): 646 | if self._last() == 'dom': 647 | # substitute the `dom` shortcut 648 | self.chain[-1] = 'document' 649 | self._add('getElementById') 650 | self._add('("{}")'.format(attr), dot=False) 651 | else: 652 | # add the item to the chain 653 | self._add(attr, dot=adot) 654 | return self 655 | 656 | def __setattr__(self, attr, value): 657 | value = JSroot._v(value) 658 | if attr == "chain" or attr == "state" or attr == "keep": 659 | # ignore our own attributes. If an attribute is added to "self" it 660 | # should be added here. I suppose this could be evaluated dynamically 661 | # using the __dict__ member. 662 | super(JSchain, self).__setattr__(attr, value) 663 | return value 664 | # print("SET", attr, value) 665 | self.setdata(attr, value) 666 | self.execExpression() 667 | 668 | def setdata(self, attr, value, adot=True): 669 | ''' 670 | Called during assigment, as in `self.js.x = 10` or during a call 671 | assignement as in `self.js.onclick = func`, where func is a function. 672 | ''' 673 | if callable(value): 674 | # is this a function call? 675 | idx = id(value) 676 | self.state.fxn[idx] = value 677 | self._add(attr, dot=adot) 678 | self._add("=function(){{server._callfxn(%s);}}" % idx, dot=False) 679 | else: 680 | # otherwise, regular assignment 681 | self._add(attr, dot=adot) 682 | self._add("=" + json.dumps(value), dot=False) 683 | return value 684 | 685 | def __setitem__(self, key, value): 686 | jkey = "['%s']" % str(key) 687 | self.setdata(jkey, value, adot=False) 688 | self.execExpression() 689 | return value 690 | 691 | def __getitem__(self, key): 692 | # all keys are strings in json, so format it 693 | key = str(key) 694 | c = self._dup() 695 | c._prepend("'%s' in " % key) 696 | haskey = c.eval() 697 | if not haskey: 698 | raise KeyError(key) 699 | jkey = "['%s']" % key 700 | c = self.getdata(jkey, adot=False) 701 | return c.eval() 702 | 703 | def __call__(self, *args, **kwargs): 704 | ''' 705 | Called when we are using in a functiion context, as in 706 | `self.js.func(15)`. 707 | ''' 708 | # evaluate the arguments 709 | p1 = [json.dumps(JSroot._v(v)) for v in args] 710 | p2 = [json.dumps(JSroot._v(v)) for k, v in kwargs.items()] 711 | s = ','.join(p1 + p2) 712 | # create the function call 713 | self._add('('+s+')', dot=False) 714 | return self 715 | 716 | def _statement(self): 717 | ''' 718 | Join all the elements and return a string representation of the 719 | Javascript expression. 720 | ''' 721 | return ''.join(self.chain) 722 | 723 | def __bytes__(self): 724 | ''' 725 | Join the elements and return as bytes encode in utf8 suitable for 726 | sending back to the browser. 727 | ''' 728 | return (''.join(self.chain)).encode("utf8") 729 | 730 | def evalAsync(self): 731 | if self.keep: 732 | stmt = self._statement() 733 | self.state.addTask(stmt) 734 | # mark it as evaluated 735 | self.keep = False 736 | 737 | def __del__(self): 738 | ''' 739 | Execute the statment when the object is deleted. 740 | 741 | An object is deleted when it goes out of scope. That's when it is put 742 | together and sent to the browser for execution. 743 | 744 | For statements, 745 | this happens when the statement ends. For example, 746 | 747 | self.js.func(1) 748 | 749 | goes out of scope when the statement after func(1). However, 750 | 751 | v = self.js.myvalue 752 | 753 | goes out of scope when the "v" goes out of scope, usually at then end of 754 | the function where it was used. In this case, the Javascript will be 755 | evaluated when "v" itself is evaluated. This happens when you perform 756 | an operation such as "v+5", saving or printing. 757 | 758 | "v" in the example above is assigned an object and not a value. This 759 | means that every time it is evaluated in an expression, it goes back 760 | to the server and retrieves the current value. 761 | 762 | On the other hand, 763 | 764 | self.v = self.js.myvalue 765 | 766 | will probably never go out of scope because it is tied to the class. 767 | To force an evaluation, call the "eval()" 768 | method, as in "self.js.myvalue.eval()". 769 | ''' 770 | if not self.keep: return 771 | # print("!!!DEL!!!") 772 | try: 773 | self.execExpression() 774 | except Exception as ex: 775 | self.state._error = ex 776 | self.state.log_error("Uncatchable exception: %s", str(ex)) 777 | raise ex 778 | 779 | def execExpression(self): 780 | # Is this a temporary expression that cannot evaluated? 781 | if self.keep: 782 | stmt = self._statement() 783 | # print("EXEC", stmt) 784 | if self.state.singleThread: 785 | # print("ASYNC0", stmt) 786 | # can't run multiple queries, so just run it async 787 | self.state.addTask(stmt) 788 | else: 789 | # otherwise, wait for evaluation 790 | # print("SYNC", stmt) 791 | try: 792 | self.eval() 793 | finally: 794 | self.keep = False 795 | 796 | # mark it as evaluated 797 | self.keep = False 798 | 799 | def eval(self, timeout=10): 800 | ''' 801 | Evaluate this object by converting it to Javascript, sending it to the browser 802 | and waiting for a response. This function is automatically called when the object 803 | is used in operators or goes out of scope so it rarely needs to 804 | be called directly. 805 | 806 | However, it is helpful 807 | to occasionally call this to avoid out-of-order results. For example, 808 | 809 | v = self.js.var1 810 | self.js.var1 = 10 811 | print(v) 812 | 813 | This will print the value 10, regardless of what var1 was before the assignment. 814 | That is because "v" is the abstract statemnt, not the evaluated value. 815 | The assigment "var1=10" is evaluated immediately. However, 816 | "v" is evaluated by the Browser 817 | when "v" is converted to a string in the print statement. If this is a problem, 818 | the code should be changed to: 819 | 820 | v = self.js.var1.eval() 821 | self.js.var1 = 10 822 | print(v) 823 | 824 | In that case, "v" is resolved immediately and hold the value of var1 before the 825 | assignment. 826 | 827 | Attributes 828 | ------------- 829 | timeout 830 | Time to wait in seconds before giving up if no response is received. 831 | ''' 832 | if not self.keep: 833 | return 0 834 | # raise ValueError("Expression cannot be evaluated") 835 | else: 836 | self.keep = False 837 | 838 | stmt = self._statement() 839 | # print("EVAL", stmt) 840 | 841 | c = self.state 842 | 843 | if not c.lock.acquire(blocking = False): 844 | c.log_error("App is active so you cannot wait for result of JS: %s" % stmt) 845 | c.addTask(stmt) 846 | return 0 847 | # raise RuntimeError("App is active so you cannot evaluate JS for: %s" % stmt) 848 | 849 | try: 850 | idx, q = c.addQuery() 851 | data = json.dumps(stmt) 852 | cmd = "sendFromBrowserToServer({}, {})".format(data, idx) 853 | c.addTask(cmd) 854 | try: 855 | c.log_message("WAITING ON RESULT QUEUE") 856 | result = q.get(timeout=timeout) 857 | c.log_message("RESULT QUEUE %s", result) 858 | c.delQuery(idx) 859 | except queue.Empty: 860 | c.log_message("TIMEOUT waiting on: %s", stmt) 861 | raise TimeoutError("Timeout waiting on: %s" % stmt) 862 | if result["error"] != "": 863 | c.log_error("ERROR EVAL %s : %s", result["error"], stmt) 864 | raise RuntimeError(result["error"] + ": " + stmt) 865 | if "value" in result: 866 | return result["value"] 867 | else: 868 | return 0 869 | finally: 870 | c.lock.release() 871 | 872 | # 873 | # Magic methods. We create these methods for force the 874 | # Javascript to be evaluated if it is used in any 875 | # opreation. 876 | # 877 | def __cmp__(self, other): return self.eval().__cmp__(other) 878 | def __eq__(self, other): return self.eval().__eq__(other) 879 | def __ne__(self, other): return self.eval().__ne__(other) 880 | def __gt__(self, other): return self.eval().__gt__(other) 881 | def __lt__(self, other): return self.eval().__lt__(other) 882 | def __ge__(self, other): return self.eval().__ge__(other) 883 | def __le__(self, other): return self.eval().__le__(other) 884 | 885 | def __pos__(self): return self.eval().__pos__() 886 | def __neg__(self): return self.eval().__neg__() 887 | def __abs__(self): return self.eval().__abs__() 888 | def __invert__(self): return self.eval().__invert__() 889 | def __round__(self, n): return self.eval().__round__(n) 890 | def __floor__(self): return self.eval().__floor__() 891 | def __ceil__(self): return self.eval().__ceil__() 892 | def __trunc__(self): return self.eval().__trunc__() 893 | 894 | def __add__(self, other): return self.eval().__add__(other) 895 | def __and__(self, other): return self.eval().__and__(other) 896 | def __div__(self, other): return self.eval().__div__(other) 897 | def __divmod__(self, other): return self.eval().__divmod__(other) 898 | def __floordiv__(self, other): return self.eval().__floordiv__(other) 899 | def __lshift__(self, other): return self.eval().__lshift__(other) 900 | def __mod__(self, other): return self.eval().__mod__(other) 901 | def __mul__(self, other): return self.eval().__mul__(other) 902 | def __or__(self, other): return self.eval().__or__(other) 903 | def __pow__(self, other): return self.eval().__pow__(other) 904 | def __rshift__(self, other): return self.eval().__rshift__(other) 905 | def __sub__(self, other): return self.eval().__sub__(other) 906 | def __truediv__(self, other): return self.eval().__truediv__(other) 907 | def __xor__(self, other): return self.eval().__xor__(other) 908 | 909 | def __radd__(self, other): return self.eval().__radd__(other) 910 | def __rand__(self, other): return self.eval().__rand__(other) 911 | def __rdiv__(self, other): return self.eval().__rdiv__(other) 912 | def __rdivmod__(self, other): return self.eval().__rdivmod__(other) 913 | def __rfloordiv__(self, other): return self.eval().__rfloordiv__(other) 914 | def __rlshift__(self, other): return self.eval().__rlshift__(other) 915 | def __rmod__(self, other): return self.eval().__rmod__(other) 916 | def __rmul__(self, other): return self.eval().__rmul__(other) 917 | def __ror__(self, other): return self.eval().__ror__(other) 918 | def __rpow__(self, other): return self.eval().__rpow__(other) 919 | def __rrshift__(self, other): return self.eval().__rrshift__(other) 920 | def __rsub__(self, other): return self.eval().__rsub__(other) 921 | def __rtruediv__(self, other): return self.eval().__rtruediv__(other) 922 | def __rxor__(self, other): return self.eval().__rxor__(other) 923 | 924 | def __coerce__(self, other): return self.eval().__coerce__(other) 925 | def __complex__(self): return self.eval().__complex__() 926 | def __float__(self): return self.eval().__float__() 927 | def __hex__(self): return self.eval().__hex__() 928 | def __index__(self): return self.eval().__index__() 929 | def __int__(self): return self.eval().__int__() 930 | def __long__(self): return self.eval().__long__() 931 | def __oct__(self): return self.eval().__oct__() 932 | def __str__(self): return self.eval().__str__() 933 | def __dir__(self): return self.eval().__dir__() 934 | def __format__(self, formatstr): return self.eval().__format__(formatstr) 935 | def __hash__(self): return self.eval().__hash__() 936 | def __nonzero__(self): return self.eval().__nonzero__() 937 | def __repr__(self): return self.eval().__repr__() 938 | def __sizeof__(self): return self.eval().__sizeof__() 939 | def __unicode__(self): return self.eval().__unicode__() 940 | 941 | def __iter__(self): return self.eval().__iter__() 942 | def __reversed__(self): return self.eval().__reversed__() 943 | def __contains__(self, item): 944 | d = self.eval() 945 | if isinstance(d, dict): 946 | # json makes all keys strings 947 | return d.__contains__(str(item)) 948 | else: 949 | return d.__contains__(item) 950 | # def __missing__(self, key): return self.eval().__missing__(key) 951 | 952 | 953 | class JSroot: 954 | ''' 955 | JS handles the lifespan of JSchain objects and things like setting 956 | and evaluation on the root object. 957 | 958 | Example: 959 | -------------- 960 | ``` 961 | state = ClientContext(AppClass) 962 | js = JSroot(state) 963 | js.document.getElementById("txt").value = 25 964 | ``` 965 | ''' 966 | 967 | def __init__(self, state): 968 | # state is a JSstate instance unique for each session 969 | self.state = state 970 | # keep track of assignments 971 | self.linkset = {} 972 | # keep track of calls 973 | self.linkcall = {} 974 | 975 | @staticmethod 976 | def _v(value): 977 | ''' 978 | If `value` is a JSchain, evaluate it. Otherwise, return value. 979 | ''' 980 | if isinstance(value, JSchain): 981 | return value.eval() 982 | else: 983 | return value 984 | 985 | def __getattr__(self, attr): 986 | ''' 987 | Called when using "." operator for the first time. Create a new chain and use it. 988 | Subsequent "." operators get processed by JSchain. 989 | ''' 990 | # rasise any pending errors; these errors can get 991 | # generate on __del__() or other places that Python 992 | # will ignore. 993 | if self.state._error: 994 | e = self.state._error 995 | self.state._error = None 996 | raise e 997 | 998 | chain = JSchain(self.state) 999 | chain._add(attr) 1000 | return chain 1001 | 1002 | def __setattr__(self, attr, value): 1003 | ''' 1004 | Called when assiging attributes. This means no JSchain was created, so just process 1005 | it directly. 1006 | ''' 1007 | # if the value to be assigned is itself a JSchain, evaluate it 1008 | value = JSroot._v(value) 1009 | # don't process our own attributes 1010 | if attr == "state" or attr == "linkset" or attr == "linkcall": 1011 | super(JSroot, self).__setattr__(attr, value) 1012 | return value 1013 | # create a new JSchain 1014 | c = self.__getattr__(attr) 1015 | c.__setattr__(None, value) 1016 | # c._add("=" + json.dumps(value), dot=False) 1017 | return c 1018 | 1019 | def __getitem__(self, key): 1020 | # this should never be called 1021 | pass 1022 | 1023 | def __setitem__(self, key, value): 1024 | value = JSroot._v(value) 1025 | if key in self.linkcall: 1026 | c = self.linkcall[key] 1027 | if isinstance(c, JSchain): 1028 | js = c._dup() 1029 | if isinstance(value, list) or isinstance(value, tuple): 1030 | js.__call__(*value) 1031 | else: 1032 | js.__call__(value) 1033 | elif callable(c): 1034 | c(value) 1035 | elif key in self.linkset: 1036 | c = self.linkset[key] 1037 | if isinstance(c, JSchain): 1038 | js = c._dup() 1039 | js._add("=" + json.dumps(value), dot=False) 1040 | 1041 | def eval(self, stmt): 1042 | ''' 1043 | Evaluate a Javascript statement `stmt` in on the Browser. 1044 | ''' 1045 | chain = JSchain(self.state) 1046 | chain._add(stmt) 1047 | return chain.eval() 1048 | 1049 | def val(self, key, callback): 1050 | self.linkset[key] = callback 1051 | callback.keep = False 1052 | 1053 | def call(self, key, callback): 1054 | self.linkcall[key] = callback 1055 | if isinstance(callback, JSchain): 1056 | callback.keep = False 1057 | 1058 | def __enter__(self): 1059 | ''' 1060 | For use in "with" statements, as in: 1061 | with server.js() as js: 1062 | js.runme() 1063 | ''' 1064 | return self 1065 | 1066 | def __exit__(self, exc_type, exc_val, exc_tb): 1067 | pass 1068 | -------------------------------------------------------------------------------- /jyserver/jscript.py: -------------------------------------------------------------------------------- 1 | import os 2 | dir = os.path.dirname(__file__) 3 | with open(dir + "/jyserver-min.js", "rb") as f: 4 | JSCRIPT = f.read() -------------------------------------------------------------------------------- /jyserver/jyserver-min.js: -------------------------------------------------------------------------------- 1 | "undefined"===typeof UID&&(UID="COOKIE");"undefined"===typeof PAGEID&&(PAGEID="COOKIE"); 2 | function evalBrowser(){var a=new XMLHttpRequest;a.onreadystatechange=function(){if(4==a.readyState&&200==a.status)try{eval(a.responseText),setTimeout(evalBrowser,1)}catch(b){console.log("ERROR",b.message),setTimeout(function(){sendErrorToServer(a.responseText,b.message);evalBrowser()},1)}};a.open("POST","/_process_srv0");a.setRequestHeader("Content-Type","application/json;charset=UTF-8");a.send(JSON.stringify({session:PAGEID,task:"next"}))} 3 | function sendFromBrowserToServer(a,b){var c="";try{var f=eval(a)}catch(e){f=0,c=e.message+": '"+a+"'",console.log("ERROR",b,c)}a=new XMLHttpRequest;a.open("POST","/_process_srv0");a.setRequestHeader("Content-Type","application/json;charset=UTF-8");a.send(JSON.stringify({session:PAGEID,task:"state",value:f,query:b,error:c}))} 4 | function sendErrorToServer(a,b){var c=new XMLHttpRequest;c.open("POST","/_process_srv0");c.setRequestHeader("Content-Type","application/json;charset=UTF-8");c.send(JSON.stringify({session:PAGEID,task:"error",error:b,expr:a}))}function closeBrowserWindow(){var a=new XMLHttpRequest;a.open("POST","/_process_srv0");a.setRequestHeader("Content-Type","application/json;charset=UTF-8");a.send(JSON.stringify({session:PAGEID,task:"unload"}))} 5 | server=new Proxy({},{get:function(a,b){return function(a){for(var c=[],e=0;e 7 | function multNum(a,b){return a*b} 8 | function fset(a){document.getElementById("time").innerHTML = a} 9 | function fset2(){document.getElementById("time").innerHTML = "T2"} 10 | function fsetApp(a,b){document.getElementById("time").innerHTML = app.addNumbers(a,b)} 11 | function faddApp(a,b){return app.addNumbers(a,b)} 12 | function fsetTestApp(){return server.setTestText()} 13 | function fsetThrow(){return server.throwError()} 14 | function add2(a,b){return a+b} 15 | 16 |

NOW

17 | ''' 18 | 19 | class TemplateApp: 20 | js = None 21 | 22 | def addNumbers(self, a, b): 23 | return a+b 24 | 25 | def setTestText(self): 26 | self.js.dom.time.innerHTML = "ABC123" 27 | 28 | def throwError(self): 29 | raise ValueError("Throw error message") 30 | 31 | class TemplateVarTest(unittest.TestCase): 32 | js = None 33 | 34 | def test_call(self): 35 | v = self.js.multNum(5,6) 36 | self.assertEqual(v, 30) 37 | self.js.fset("TEST123") 38 | self.assertEqual(self.js.dom.time.innerHTML, "TEST123") 39 | self.js.fset2() 40 | self.assertEqual(self.js.dom.time.innerHTML, "T2") 41 | self.js.fsetTestApp() 42 | self.assertEqual(self.js.dom.time.innerHTML, "ABC123") 43 | # self.js.fsetApp(12, 20) 44 | # self.assertEqual(self.js.dom.time.innerHTML, "32") 45 | 46 | def test_float(self): 47 | self.js.valfloat = 1.5 48 | self.assertTrue(self.js.valfloat * 2 == 3.0) 49 | 50 | def test_dict(self): 51 | self.js.valfloat = 1.5 52 | self.assertTrue(self.js.valfloat * 2 == 3.0) 53 | self.js.dict = {5:-99,9:-999} 54 | self.assertTrue(5 in self.js.dict) 55 | self.assertEqual(self.js.dict[9], -999) 56 | 57 | with self.assertRaises(KeyError): 58 | self.js.dict[1] 59 | 60 | self.js.dict[10] = 45.175 61 | self.assertIn(10, self.js.dict) 62 | self.assertEqual(self.js.dict[10], 45.175) 63 | 64 | def test_array(self): 65 | self.js.arr = [5,'10',15] 66 | self.assertEqual(self.js.arr, [5, '10', 15]) 67 | self.js.arr += [30, 32] 68 | self.assertEqual(self.js.arr, [5, '10', 15, 30, 32]) 69 | 70 | def test_arith(self): 71 | self.js.counter = 0 72 | v = self.js.counter 73 | self.assertEqual(self.js.counter, 0) 74 | self.js.counter = 1 75 | self.assertEqual(self.js.counter, 1) 76 | self.js.counter += 10 77 | self.assertEqual(self.js.counter, 11) 78 | self.js.counter = 100 + self.js.counter 79 | self.assertEqual(self.js.counter, 111) 80 | self.assertEqual(v, 111) 81 | 82 | def test_dom(self): 83 | self.js.dom.time.innerHTML = "{:.1f}".format(self.js.counter) 84 | self.assertEqual(self.js.dom.time.innerHTML, "111.0") 85 | self.js.dom.time.xyz = "abc" 86 | self.assertEqual(self.js.dom.time.xyz, "abc") 87 | self.js.dom.time.innerHTML = "DONE" 88 | self.assertEqual(self.js.dom.time.innerHTML, "DONE") 89 | 90 | def test_dom_excpetion(self): 91 | with self.assertRaises(RuntimeError): 92 | print(self.js.dom.time1.innerHTML) 93 | with self.assertRaises(RuntimeError): 94 | self.js.dom.time1.innerHTML = "thisfail" -------------------------------------------------------------------------------- /tests/templates/flask-simple.html: -------------------------------------------------------------------------------- 1 |

NOW

2 | -------------------------------------------------------------------------------- /tests/templates/flask1.html: -------------------------------------------------------------------------------- 1 | 2 | 12 |

NOW

13 | -------------------------------------------------------------------------------- /tests/test_bottle.py: -------------------------------------------------------------------------------- 1 | import context 2 | 3 | from bottle import route, run 4 | import jyserver.Bottle as js 5 | 6 | import unittest 7 | from template_vars import TemplateApp, TemplateVarTest, test_html 8 | 9 | @js.use 10 | class App(TemplateApp): 11 | pass 12 | 13 | @route('/') 14 | def hello_world(): 15 | html = test_html 16 | return App.render(html) 17 | 18 | @js.task 19 | def runApp(): 20 | # import asyncio 21 | # asyncio.set_event_loop(asyncio.new_event_loop()) 22 | # run(port=8080, server='tornado') 23 | run(port=8080) 24 | 25 | if __name__ == '__main__': 26 | 27 | TemplateVarTest.js = App.getJS() 28 | runApp() 29 | # import webbrowser 30 | # webbrowser.open(f'http://localhost:{httpd.port}') 31 | unittest.main() -------------------------------------------------------------------------------- /tests/test_class_main.py: -------------------------------------------------------------------------------- 1 | #!/usr/env/bin python3 2 | 3 | from context import jyserver 4 | from jyserver.Server import Server, Client 5 | import time 6 | 7 | class App(Client): 8 | def __init__(self): 9 | self.html = ''' 10 |

NOW

11 | 12 | 13 | ''' 14 | self.start0 = time.time() 15 | self.running = True 16 | 17 | def reset(self): 18 | self.start0 = time.time() 19 | self.js.dom.time.innerHTML = "{:.1f}".format(0) 20 | 21 | def stop(self): 22 | self.running = False 23 | self.js.dom.b2.innerHTML = "Restart" 24 | self.js.dom.b2.onclick = self.restart 25 | 26 | def restart(self): 27 | self.running = True 28 | self.js.dom.b2.innerHTML = "Pause" 29 | self.js.dom.b2.onclick = self.stop 30 | 31 | def main(self): 32 | for _ in range(100): 33 | if self.running: 34 | self.js.dom.time.innerHTML = "{:.1f}".format(time.time() - self.start0) 35 | time.sleep(1) 36 | 37 | httpd = Server(App, verbose=False) 38 | print("serving at port", httpd.port) 39 | import webbrowser 40 | # webbrowser.open(f'http://localhost:{httpd.port}') 41 | httpd.start() -------------------------------------------------------------------------------- /tests/test_clock.py: -------------------------------------------------------------------------------- 1 | #!/usr/env/bin python3 2 | 3 | from context import jyserver 4 | from jyserver.Server import Server, Client 5 | import time 6 | 7 | class App(Client): 8 | def __init__(self): 9 | self.start0 = time.time() 10 | self.running = True 11 | 12 | def isRunning(self): 13 | return self.running 14 | 15 | def failtask(self, v, v2): 16 | print(v, v2) 17 | print(self.js.dom.time.innerHTML) 18 | 19 | def reenter(self, v, v2): 20 | print(v, v2) 21 | self.js.dom.tx.innerHTML = "Tested!" 22 | 23 | def reset(self): 24 | self.start0 = time.time() 25 | self.js.dom.time.innerHTML = "{:.1f}".format(0) 26 | 27 | def start(self): 28 | self.running = True 29 | 30 | def stop(self): 31 | self.running = False 32 | self.js.dom.b2.innerHTML = "Restart" 33 | self.js.dom.b2.onclick = self.restart 34 | 35 | def restart(self): 36 | self.running = True 37 | self.js.dom.b2.innerHTML = "Pause" 38 | self.js.dom.b2.onclick = self.stop 39 | 40 | def clickme(self): 41 | count = 0 42 | page = self.h(html=""" 43 | Please click again and again:

COUNT

44 | Or go back' 45 | """) 46 | while page.alive(): 47 | count += 1 48 | self.js.dom.text.innerHTML = count 49 | time.sleep(1) 50 | print("clickme done") 51 | 52 | def index(self): 53 | page = self.h(html=""" 54 | 62 |

R

63 |

NOW

64 | 65 | 66 | 67 | 68 | 69 | page2 70 | """) 71 | while page.alive(): 72 | if self.running: 73 | self.js.dom.time.innerHTML = "{:.1f}".format(time.time() - self.start0) 74 | time.sleep(1) 75 | print("index done") 76 | 77 | httpd = Server(App, verbose=True) 78 | print("serving at port", httpd.port) 79 | # import webbrowser 80 | # webbrowser.open(f'http://localhost:{httpd.port}') 81 | httpd.start(cookies=True) -------------------------------------------------------------------------------- /tests/test_clock_counter.py: -------------------------------------------------------------------------------- 1 | #!/usr/env/bin python3 2 | 3 | from context import jyserver 4 | from jyserver.Server import Server, Client 5 | import time 6 | 7 | class App(Client): 8 | def __init__(self): 9 | self.start0 = time.time() 10 | def index(self): 11 | self.h(html = ''' 12 | All browser tabs should be the same 13 |

WHEN

14 | ''') 15 | for _ in range(100): 16 | self.js.dom.time.innerHTML = "{:.1f}".format(time.time() - self.start0) 17 | time.sleep(1) 18 | 19 | httpd = Server(App) 20 | print("serving at port", httpd.port) 21 | # import webbrowser 22 | # webbrowser.open(f'http://localhost:{httpd.port}') 23 | httpd.start() -------------------------------------------------------------------------------- /tests/test_clock_nocookies.py: -------------------------------------------------------------------------------- 1 | #!/usr/env/bin python3 2 | 3 | from context import jyserver 4 | from jyserver.Server import Server, Client 5 | import time 6 | 7 | class App(Client): 8 | def __init__(self): 9 | self.start0 = time.time() 10 | def index(self): 11 | self.h(html = ''' 12 | All browser tabs should be different 13 |

WHEN

14 | ''') 15 | # self.start0 = time.time() 16 | for _ in range(100): 17 | self.js.dom.time.innerHTML = "{:.1f}".format(time.time() - self.start0) 18 | time.sleep(1) 19 | 20 | httpd = Server(App, verbose=False) 21 | print("serving at port", httpd.port) 22 | # import webbrowser 23 | # webbrowser.open(f'http://localhost:{httpd.port}') 24 | httpd.start(cookies=False) -------------------------------------------------------------------------------- /tests/test_django.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ftrias/jyserver/66b9946b973166c496075bdb13e189aa9c66c57f/tests/test_django.py -------------------------------------------------------------------------------- /tests/test_flask.py: -------------------------------------------------------------------------------- 1 | import context 2 | 3 | from flask import Flask, render_template, request 4 | import jyserver.Flask as js 5 | 6 | import unittest 7 | from template_vars import TemplateApp, TemplateVarTest, test_html 8 | 9 | app = Flask(__name__) 10 | 11 | @js.use(app) 12 | class App(TemplateApp): 13 | pass 14 | 15 | @app.route('/') 16 | def hello_world(): 17 | html = test_html 18 | return App.render(html) 19 | 20 | @js.task 21 | def runApp(): 22 | app.run(port=8080) 23 | 24 | if __name__ == '__main__': 25 | 26 | TemplateVarTest.js = App.getJS() 27 | runApp() 28 | # import webbrowser 29 | # webbrowser.open(f'http://localhost:{httpd.port}') 30 | unittest.main() -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | #!/usr/env/bin python3 2 | 3 | from context import jyserver 4 | from jyserver.Server import Server, Client 5 | import time 6 | 7 | class App(Client): 8 | def __init__(self): 9 | self.html = ''' 10 |

NOW

11 | ''' 12 | 13 | httpd = Server(App, verbose=False) 14 | print("serving at port", httpd.port) 15 | # import webbrowser 16 | # webbrowser.open(f'http://localhost:{httpd.port}') 17 | httpd.start(wait=False) 18 | 19 | start0 = time.time() 20 | js = httpd.js() 21 | for _ in range(100): 22 | js.dom.time.innerHTML = "{:.1f}".format(time.time() - start0) 23 | time.sleep(1) -------------------------------------------------------------------------------- /tests/test_throws.py: -------------------------------------------------------------------------------- 1 | #!/usr/env/bin python3 2 | 3 | import unittest 4 | from context import jyserver 5 | from jyserver.Server import Server, Client 6 | import time 7 | 8 | class App(Client): 9 | def __init__(self): 10 | self.html = ''' 11 | 30 |

NOW

31 | ''' 32 | def addNumbers(self, a, b): 33 | return a/0 34 | 35 | def throwError(self): 36 | raise ValueError("Fatal error from server") 37 | 38 | class MyTest(unittest.TestCase): 39 | @classmethod 40 | def setUpClass(self): 41 | global httpd 42 | self.js = httpd.js() 43 | 44 | @classmethod 45 | def tearDownClass(self): 46 | global httpd 47 | httpd.stop() 48 | 49 | def test_browser_raise(self): 50 | # this throws in the browser's context 51 | x = self.js.fsetThrow() 52 | self.assertEqual(x, 2) 53 | 54 | def test_call_throw(self): 55 | with self.assertRaises(RuntimeError): 56 | self.js.fThrow(0).eval() 57 | 58 | def test_dict(self): 59 | self.js.dict = {5:10} 60 | with self.assertRaises(KeyError): 61 | self.js.dict[1] 62 | 63 | def test_dom_excpetion(self): 64 | with self.assertRaises(RuntimeError): 65 | print(self.js.dom.time1.innerHTML) 66 | with self.assertRaises(RuntimeError): 67 | self.js.dom.time1.innerHTML = "thisfail" 68 | 69 | if __name__ == '__main__': 70 | 71 | httpd = Server(App, verbose=False) 72 | print("serving at port", httpd.port) 73 | import webbrowser 74 | # webbrowser.open(f'http://localhost:{httpd.port}') 75 | httpd.start(wait=False) 76 | 77 | unittest.main() -------------------------------------------------------------------------------- /tests/test_vars.py: -------------------------------------------------------------------------------- 1 | from context import jyserver 2 | 3 | from jyserver.Server import Server, Client 4 | 5 | import unittest 6 | from template_vars import TemplateApp, TemplateVarTest, test_html 7 | 8 | class App(TemplateApp): 9 | def __init__(self): 10 | self.html = test_html 11 | 12 | if __name__ == '__main__': 13 | 14 | httpd = Server(App, verbose=False, port=8080) 15 | TemplateVarTest.js = httpd.js() 16 | 17 | print("serving at port", httpd.port) 18 | # import webbrowser 19 | # webbrowser.open(f'http://localhost:{httpd.port}') 20 | httpd.start(wait=False) 21 | 22 | unittest.main() 23 | httpd.stop() --------------------------------------------------------------------------------